managed-deepagents 0.5.4-dev.2 → 0.5.4-dev.21

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.
Files changed (74) hide show
  1. package/README.md +76 -18
  2. package/dist/channels/manifest.d.ts +2 -0
  3. package/dist/channels/manifest.d.ts.map +1 -1
  4. package/dist/channels/slack/index.d.ts +25 -71
  5. package/dist/channels/slack/index.d.ts.map +1 -1
  6. package/dist/channels/slack/index.js +47 -115
  7. package/dist/channels/slack/index.js.map +1 -1
  8. package/dist/channels/trigger/client.d.ts +30 -0
  9. package/dist/channels/trigger/client.d.ts.map +1 -0
  10. package/dist/channels/trigger/client.js +118 -0
  11. package/dist/channels/trigger/client.js.map +1 -0
  12. package/dist/channels/trigger/http.d.ts +24 -0
  13. package/dist/channels/trigger/http.d.ts.map +1 -0
  14. package/dist/channels/trigger/http.js +138 -0
  15. package/dist/channels/trigger/http.js.map +1 -0
  16. package/dist/channels/trigger/types.d.ts +46 -0
  17. package/dist/channels/trigger/types.d.ts.map +1 -0
  18. package/dist/channels/trigger/types.js +28 -0
  19. package/dist/channels/trigger/types.js.map +1 -0
  20. package/dist/channels/trigger/uuid.d.ts +3 -0
  21. package/dist/channels/trigger/uuid.d.ts.map +1 -0
  22. package/dist/channels/trigger/uuid.js +22 -0
  23. package/dist/channels/trigger/uuid.js.map +1 -0
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/ingress/index.d.ts +20 -0
  27. package/dist/ingress/index.d.ts.map +1 -1
  28. package/dist/ingress/index.js +44 -1
  29. package/dist/ingress/index.js.map +1 -1
  30. package/dist/namespaces.d.ts +8 -6
  31. package/dist/namespaces.d.ts.map +1 -1
  32. package/dist/namespaces.js +1 -1
  33. package/dist/namespaces.js.map +1 -1
  34. package/dist/runtime/auth.d.ts +3 -0
  35. package/dist/runtime/auth.d.ts.map +1 -1
  36. package/dist/runtime/auth.js +20 -10
  37. package/dist/runtime/auth.js.map +1 -1
  38. package/dist/runtime/backend.d.ts +0 -1
  39. package/dist/runtime/backend.d.ts.map +1 -1
  40. package/dist/runtime/backend.js +39 -8
  41. package/dist/runtime/backend.js.map +1 -1
  42. package/dist/runtime/channel.d.ts +5 -4
  43. package/dist/runtime/channel.d.ts.map +1 -1
  44. package/dist/runtime/channel.js.map +1 -1
  45. package/dist/runtime/identity-http.d.ts +3 -3
  46. package/dist/runtime/identity-http.d.ts.map +1 -1
  47. package/dist/runtime/identity-http.js +53 -12
  48. package/dist/runtime/identity-http.js.map +1 -1
  49. package/dist/runtime/index.d.ts.map +1 -1
  50. package/dist/runtime/index.js +1 -2
  51. package/dist/runtime/index.js.map +1 -1
  52. package/dist/runtime/sandbox-manager.d.ts +41 -45
  53. package/dist/runtime/sandbox-manager.d.ts.map +1 -1
  54. package/dist/runtime/sandbox-manager.js +143 -135
  55. package/dist/runtime/sandbox-manager.js.map +1 -1
  56. package/dist/runtime/sandbox-names.d.ts +9 -0
  57. package/dist/runtime/sandbox-names.d.ts.map +1 -0
  58. package/dist/runtime/sandbox-names.js +9 -0
  59. package/dist/runtime/sandbox-names.js.map +1 -0
  60. package/dist/runtime/types.d.ts +0 -5
  61. package/dist/runtime/types.d.ts.map +1 -1
  62. package/dist/sandbox.d.ts +61 -15
  63. package/dist/sandbox.d.ts.map +1 -1
  64. package/dist/sandbox.js +25 -7
  65. package/dist/sandbox.js.map +1 -1
  66. package/package.json +8 -7
  67. package/dist/runtime/sandbox-stream-retry.d.ts +0 -18
  68. package/dist/runtime/sandbox-stream-retry.d.ts.map +0 -1
  69. package/dist/runtime/sandbox-stream-retry.js +0 -90
  70. package/dist/runtime/sandbox-stream-retry.js.map +0 -1
  71. package/dist/runtime/setup-script.d.ts +0 -18
  72. package/dist/runtime/setup-script.d.ts.map +0 -1
  73. package/dist/runtime/setup-script.js +0 -49
  74. package/dist/runtime/setup-script.js.map +0 -1
@@ -1,39 +1,40 @@
1
- import type { SandboxBackendProtocolV2 } from "deepagents";
1
+ import { type SandboxBackendProtocolV2 } from "deepagents";
2
2
  import type { IdentityDefinition } from "../identity/index.js";
3
3
  import type { SandboxDefinition } from "../sandbox.js";
4
4
  import { type Connector } from "./connector.js";
5
5
  import type { ManagedRunConfig } from "./types.js";
6
- type LangSmithSandboxClientLoader = () => Promise<{
7
- SandboxClient: new (options?: {
8
- apiKey?: string;
9
- }) => {
10
- createSandbox(options?: Record<string, unknown>): Promise<unknown>;
11
- getSandbox?(name: string): Promise<unknown>;
12
- startSandbox?(name: string): Promise<unknown>;
13
- deleteSandbox?(name: string): Promise<void>;
14
- };
15
- }>;
6
+ /** Deployment secret naming the recipe snapshot baked at `mda deploy`. */
7
+ export declare const RECIPE_SNAPSHOT_ENV = "MDA_SANDBOX_RECIPE_SNAPSHOT";
8
+ /**
9
+ * Env var naming the workspace the CLI baked the recipe snapshot into.
10
+ *
11
+ * `mda deploy` sets it on the deployment and `mda dev` stages it into the local
12
+ * `.env`. Sandboxes and snapshots are workspace-scoped, so the runtime must
13
+ * send it too — an API key whose default workspace differs from this one
14
+ * resolves `snapshotName` in the wrong workspace and 404s on the recipe.
15
+ */
16
+ export declare const WORKSPACE_ID_ENV = "LANGSMITH_WORKSPACE_ID";
16
17
  /** Optional runtime inputs that drive connector sandbox provisioning. */
17
18
  export interface ResolveManagedSandboxOptions {
18
19
  connectors?: Connector[];
19
20
  identity?: IdentityDefinition;
20
21
  }
21
22
  /**
22
- * Resolve (and cache) the scoped LangSmith sandbox backend for this run,
23
- * provisioning it with `setup.sh` the first time it is created.
23
+ * Resolve (and cache) the per-thread LangSmith sandbox backend for this run.
24
+ * When deploy baked a recipe, new sandboxes clone it; otherwise Host supplies
25
+ * its default image.
24
26
  *
25
27
  * Connectors that implement {@link Connector.sandbox} run on create, when a
26
28
  * new thread starts against a reused sandbox (mode `"reuse"`), and on later
27
29
  * messages in an already-synced thread (mode `"credentials"`).
28
30
  */
29
- export declare function resolveManagedSandbox(sandbox: SandboxDefinition, setupScript: string | undefined, config: ManagedRunConfig | undefined, options?: ResolveManagedSandboxOptions): Promise<SandboxBackendProtocolV2>;
30
- /** Derive the process-level reuse key from the configured sandbox scope. */
31
- export declare function sandboxScopeKey(sandbox: SandboxDefinition, config: ManagedRunConfig | undefined): string;
31
+ export declare function resolveManagedSandbox(sandbox: SandboxDefinition, config: ManagedRunConfig | undefined, options?: ResolveManagedSandboxOptions): Promise<SandboxBackendProtocolV2>;
32
+ /** Derive the process-level reuse key (always one sandbox per thread). */
33
+ export declare function sandboxScopeKey(config: ManagedRunConfig | undefined): string;
32
34
  /** Clear the process-level sandbox cache. Test-only. */
33
35
  export declare function clearManagedSandboxCacheForTests(): void;
34
36
  /** Options that pin invalidation to the sandbox that actually failed. */
35
37
  export interface InvalidateManagedSandboxOptions {
36
- setupScript?: string;
37
38
  /** Backend id captured before the failing operation. */
38
39
  expectedBackendId?: string;
39
40
  /** Host box name captured before the failing operation. */
@@ -43,7 +44,7 @@ export interface InvalidateManagedSandboxOptions {
43
44
  * Drop a cached sandbox and delete its Host box so recreate cannot adopt it.
44
45
  *
45
46
  * Process-cache clear alone is not enough under deploy: names are deterministic
46
- * per scope, so the next resolve would re-adopt the same broken box. Mark the
47
+ * per thread, so the next resolve would re-adopt the same broken box. Mark the
47
48
  * name blocked (delete can lag) and best-effort `deleteSandbox`.
48
49
  *
49
50
  * When `expectedBackendId` / `expectedHostName` are set, only pop the cache
@@ -51,18 +52,11 @@ export interface InvalidateManagedSandboxOptions {
51
52
  * already have replaced it — deleting the replacement would strand the healthy
52
53
  * recreate.
53
54
  */
54
- export declare function invalidateManagedSandbox(sandbox: SandboxDefinition, config: ManagedRunConfig | undefined, setupScriptOrOptions?: string | InvalidateManagedSandboxOptions): Promise<void>;
55
+ export declare function invalidateManagedSandbox(sandbox: SandboxDefinition, config: ManagedRunConfig | undefined, options?: InvalidateManagedSandboxOptions): Promise<void>;
55
56
  /** Host box name recorded for a resolved backend instance. */
56
57
  export declare function managedSandboxHostName(backend: object): string | undefined;
57
58
  /** True when a sandbox API error is worth invalidating + recreating once. */
58
59
  export declare function isTransientSandboxError(error: unknown): boolean;
59
- /** Override the LangSmith SDK loader. Test-only. */
60
- export declare function setLangSmithSandboxClientLoaderForTests(loader?: LangSmithSandboxClientLoader): void;
61
- /** Override the DeepAgents LangSmith backend wrapper. Test-only. */
62
- export declare function setLangSmithSandboxBackendForTests(backend?: new (options: {
63
- sandbox: unknown;
64
- defaultTimeout?: number;
65
- }) => object): void;
66
60
  /**
67
61
  * Deployment this runtime belongs to, used to name the sandboxes it creates so
68
62
  * `mda delete` can find them again.
@@ -72,27 +66,29 @@ export declare function setLangSmithSandboxBackendForTests(backend?: new (option
72
66
  * local `mda dev`, where sandboxes stay unnamed and LangSmith generates a name.
73
67
  */
74
68
  export declare function managedDeploymentName(): string | undefined;
75
- /** How a sandbox was provisioned, beyond the scope it serves. */
76
- export interface SandboxProvisioning {
77
- /** Options handed to `createSandbox` (snapshot, sizing, TTLs). */
78
- createOptions?: Record<string, unknown>;
79
- /** Contents of the project's `setup.sh`, if it has one. */
80
- setupScript?: string;
81
- }
82
69
  /**
83
- * Deterministic sandbox name for a deployment, sandbox scope, and provisioning
84
- * recipe.
70
+ * Deterministic sandbox name for a deployment and thread.
85
71
  *
86
- * The inputs are digested rather than embedded because the scope key carries
87
- * thread and assistant UUIDs; a short digest keeps the name clear of any length
88
- * limit while staying stable across container restarts, which is what lets a
89
- * restarted deployment re-adopt its sandbox instead of leaking it.
72
+ * The thread key is digested rather than embedded so names stay short and
73
+ * stable across container restarts. Recipe (`setup.sh` / bake base) changes do
74
+ * **not** rename live boxes new threads clone the new recipe; live threads
75
+ * keep `/workspace` until idle reclaim.
76
+ */
77
+ export declare function managedSandboxName(deployment: string, scopeKey: string): string;
78
+ /**
79
+ * Options for `createSandbox`. Thread boxes clone the deploy-baked recipe when
80
+ * present; author `snapshotName` / `snapshotId` / `dockerImage` are bake bases
81
+ * only and are not passed here.
82
+ */
83
+ export declare function sandboxCreateOptions(sandbox: SandboxDefinition): Record<string, unknown>;
84
+ /**
85
+ * Constructor options for the LangSmith sandbox client.
90
86
  *
91
- * Provisioning is part of the identity so that editing `setup.sh` or switching
92
- * snapshots produces a different name. An adopted sandbox skips provisioning, so
93
- * without this a redeploy would silently leave existing threads on the old
94
- * environment.
87
+ * Both the API key and the workspace scope come from the environment `mda`
88
+ * wrote, so every sandbox call lands in the workspace holding the baked recipe.
95
89
  */
96
- export declare function managedSandboxName(deployment: string, scopeKey: string, provisioning?: SandboxProvisioning): string;
97
- export {};
90
+ export declare function sandboxClientOptions(sandbox: SandboxDefinition): {
91
+ apiKey?: string;
92
+ headers?: Record<string, string>;
93
+ } | undefined;
98
94
  //# sourceMappingURL=sandbox-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-manager.d.ts","sourceRoot":"","sources":["../../src/runtime/sandbox-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sandbox-manager.d.ts","sourceRoot":"","sources":["../../src/runtime/sandbox-manager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAEjE;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AAuEzD,yEAAyE;AACzE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,OAAO,CAAC,EAAE,4BAA4B,GACrC,OAAO,CAAC,wBAAwB,CAAC,CAwEnC;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,GAAG,MAAM,CAE5E;AAED,wDAAwD;AACxD,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;AAED,yEAAyE;AACzE,MAAM,WAAW,+BAA+B;IAC9C,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAiEf;AAED,8DAA8D;AAC9D,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1E;AA+BD,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAe/D;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAK1D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAMR;AAkED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBzB;AAYD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GACzB;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,SAAS,CAYnE"}
@@ -1,17 +1,37 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
+ import { LangSmithSandbox } from "deepagents";
3
+ import { Sandbox, SandboxClient } from "langsmith/sandbox";
2
4
  import { hasSandboxConnectors, runConnectorSandboxHooks, } from "./connector.js";
3
- import { runSetupScript } from "./setup-script.js";
4
- import { isSandboxStreamError } from "./sandbox-stream-retry.js";
5
+ import { HOST_SANDBOX_NAME_LIMIT } from "./sandbox-names.js";
6
+ /** Deployment secret naming the recipe snapshot baked at `mda deploy`. */
7
+ export const RECIPE_SNAPSHOT_ENV = "MDA_SANDBOX_RECIPE_SNAPSHOT";
5
8
  /**
6
- * Created sandboxes, keyed by scope, reused for the lifetime of this runtime
7
- * process. Storing the in-flight promise dedupes concurrent runs and guarantees
8
- * `setup.sh` is executed only once per newly created sandbox.
9
+ * Env var naming the workspace the CLI baked the recipe snapshot into.
10
+ *
11
+ * `mda deploy` sets it on the deployment and `mda dev` stages it into the local
12
+ * `.env`. Sandboxes and snapshots are workspace-scoped, so the runtime must
13
+ * send it too — an API key whose default workspace differs from this one
14
+ * resolves `snapshotName` in the wrong workspace and 404s on the recipe.
15
+ */
16
+ export const WORKSPACE_ID_ENV = "LANGSMITH_WORKSPACE_ID";
17
+ /** Header the LangSmith APIs use to scope a request to one workspace. */
18
+ const TENANT_HEADER = "X-Tenant-Id";
19
+ /**
20
+ * Created sandboxes, keyed by thread, reused for the lifetime of this runtime
21
+ * process. Storing the in-flight promise dedupes concurrent runs. Provisioning
22
+ * (`setup.sh`) happens at deploy bake, not on thread create.
9
23
  */
10
24
  const managedSandboxes = new Map();
11
25
  /** Host box names that must not be re-adopted after invalidate until recreate. */
12
26
  const invalidatedSandboxNames = new Set();
13
27
  /** Host box name for a resolved backend instance (survives cache replacement). */
14
28
  const backendHostNames = new WeakMap();
29
+ /** Cold-box race: command stream died before the sandbox accepted the command. */
30
+ const STREAM_NEVER_STARTED_RE = /Command stream ended before ['"]started['"] message/i;
31
+ const STREAM_NEVER_STARTED_NAMES = new Set([
32
+ "_StreamEndedBeforeStarted",
33
+ "StreamEndedBeforeStarted",
34
+ ]);
15
35
  /**
16
36
  * Separator between the deployment name and the identity digest in a managed
17
37
  * sandbox name.
@@ -24,32 +44,28 @@ const backendHostNames = new WeakMap();
24
44
  const SANDBOX_NAME_SEPARATOR = "--";
25
45
  /** Hex characters of the identity digest kept in a sandbox name. */
26
46
  const SANDBOX_DIGEST_LENGTH = 12;
47
+ const SANDBOX_FALLBACK_SUFFIX_LENGTH = 9;
48
+ const MAX_PRIMARY_DEPLOYMENT_COMPONENT_LENGTH = HOST_SANDBOX_NAME_LIMIT -
49
+ SANDBOX_NAME_SEPARATOR.length -
50
+ SANDBOX_DIGEST_LENGTH;
51
+ const MAX_FALLBACK_DEPLOYMENT_COMPONENT_LENGTH = MAX_PRIMARY_DEPLOYMENT_COMPONENT_LENGTH - SANDBOX_FALLBACK_SUFFIX_LENGTH;
52
+ const SANDBOX_DEPLOYMENT_DIGEST_LENGTH = 8;
27
53
  /** Sandbox statuses that can be adopted as-is. */
28
54
  const ADOPTABLE_STATUS = new Set(["ready", "running"]);
29
55
  /** Sandbox status for one that is stopped but restartable. */
30
56
  const STOPPED_STATUS = "stopped";
31
- const defaultLangSmithSandboxClientLoader = async () => {
32
- const dynamicImport = new Function("specifier", "return import(specifier)");
33
- return (await dynamicImport("langsmith/sandbox"));
34
- };
35
- let langSmithSandboxClientLoader = defaultLangSmithSandboxClientLoader;
36
- const defaultLangSmithSandboxBackendLoader = async () => {
37
- const { LangSmithSandbox } = await import("deepagents");
38
- return LangSmithSandbox;
39
- };
40
- let langSmithSandboxBackendLoader = defaultLangSmithSandboxBackendLoader;
41
57
  /**
42
- * Resolve (and cache) the scoped LangSmith sandbox backend for this run,
43
- * provisioning it with `setup.sh` the first time it is created.
58
+ * Resolve (and cache) the per-thread LangSmith sandbox backend for this run.
59
+ * When deploy baked a recipe, new sandboxes clone it; otherwise Host supplies
60
+ * its default image.
44
61
  *
45
62
  * Connectors that implement {@link Connector.sandbox} run on create, when a
46
63
  * new thread starts against a reused sandbox (mode `"reuse"`), and on later
47
64
  * messages in an already-synced thread (mode `"credentials"`).
48
65
  */
49
- export function resolveManagedSandbox(sandbox, setupScript, config, options) {
50
- const key = sandboxScopeKey(sandbox, config);
66
+ export function resolveManagedSandbox(sandbox, config, options) {
67
+ const key = sandboxScopeKey(config);
51
68
  const hasSandbox = hasSandboxConnectors(options?.connectors);
52
- const threadKey = threadSyncKey(sandbox, config);
53
69
  let entry = managedSandboxes.get(key);
54
70
  if (!entry) {
55
71
  const created = {
@@ -57,7 +73,7 @@ export function resolveManagedSandbox(sandbox, setupScript, config, options) {
57
73
  syncChain: Promise.resolve(),
58
74
  freshlyCreated: true,
59
75
  ready: Promise.resolve().then(async () => {
60
- const { backend, hostName } = await createLangSmithSandbox(sandbox, setupScript, config, options, "create", key);
76
+ const { backend, hostName } = await createLangSmithSandbox(sandbox, config, options, "create", key);
61
77
  created.hostName = hostName;
62
78
  const id = backend.id;
63
79
  created.backendId =
@@ -65,7 +81,7 @@ export function resolveManagedSandbox(sandbox, setupScript, config, options) {
65
81
  if (hostName) {
66
82
  backendHostNames.set(backend, hostName);
67
83
  }
68
- created.syncedThreads.add(threadKey);
84
+ created.syncedThreads.add(key);
69
85
  return backend;
70
86
  }),
71
87
  };
@@ -89,7 +105,7 @@ export function resolveManagedSandbox(sandbox, setupScript, config, options) {
89
105
  cached.freshlyCreated = false;
90
106
  return;
91
107
  }
92
- if (cached.syncedThreads.has(threadKey)) {
108
+ if (cached.syncedThreads.has(key)) {
93
109
  // Same thread: refresh credentials / CLI wiring without re-cloning.
94
110
  // Otherwise redeploys that fix auth (e.g. gh wrapper) never apply mid-thread.
95
111
  await runConnectorSandboxHooks(options?.connectors, {
@@ -107,18 +123,14 @@ export function resolveManagedSandbox(sandbox, setupScript, config, options) {
107
123
  config,
108
124
  identity: options?.identity,
109
125
  });
110
- cached.syncedThreads.add(threadKey);
126
+ cached.syncedThreads.add(key);
111
127
  });
112
128
  await cached.syncChain;
113
129
  return backend;
114
130
  });
115
131
  }
116
- /** Derive the process-level reuse key from the configured sandbox scope. */
117
- export function sandboxScopeKey(sandbox, config) {
118
- const scope = sandbox.options?.scope ?? "thread";
119
- if (scope === "agent") {
120
- return `agent:${assistantIdFromConfig(config)}`;
121
- }
132
+ /** Derive the process-level reuse key (always one sandbox per thread). */
133
+ export function sandboxScopeKey(config) {
122
134
  return `thread:${threadIdFromConfig(config)}`;
123
135
  }
124
136
  /** Clear the process-level sandbox cache. Test-only. */
@@ -130,7 +142,7 @@ export function clearManagedSandboxCacheForTests() {
130
142
  * Drop a cached sandbox and delete its Host box so recreate cannot adopt it.
131
143
  *
132
144
  * Process-cache clear alone is not enough under deploy: names are deterministic
133
- * per scope, so the next resolve would re-adopt the same broken box. Mark the
145
+ * per thread, so the next resolve would re-adopt the same broken box. Mark the
134
146
  * name blocked (delete can lag) and best-effort `deleteSandbox`.
135
147
  *
136
148
  * When `expectedBackendId` / `expectedHostName` are set, only pop the cache
@@ -138,13 +150,9 @@ export function clearManagedSandboxCacheForTests() {
138
150
  * already have replaced it — deleting the replacement would strand the healthy
139
151
  * recreate.
140
152
  */
141
- export async function invalidateManagedSandbox(sandbox, config, setupScriptOrOptions) {
142
- const options = typeof setupScriptOrOptions === "string" ||
143
- setupScriptOrOptions === undefined
144
- ? { setupScript: setupScriptOrOptions }
145
- : setupScriptOrOptions;
146
- const { setupScript, expectedBackendId, expectedHostName } = options;
147
- const key = sandboxScopeKey(sandbox, config);
153
+ export async function invalidateManagedSandbox(sandbox, config, options = {}) {
154
+ const { expectedBackendId, expectedHostName } = options;
155
+ const key = sandboxScopeKey(config);
148
156
  const names = new Set();
149
157
  let popCache = true;
150
158
  let entry = managedSandboxes.get(key);
@@ -180,7 +188,7 @@ export async function invalidateManagedSandbox(sandbox, config, setupScriptOrOpt
180
188
  expectedBackendId === undefined &&
181
189
  expectedHostName === undefined) {
182
190
  try {
183
- const computed = hostSandboxNameFor(sandbox, config, setupScript);
191
+ const computed = hostSandboxNameFor(config);
184
192
  if (computed) {
185
193
  names.add(computed);
186
194
  }
@@ -201,11 +209,33 @@ export async function invalidateManagedSandbox(sandbox, config, setupScriptOrOpt
201
209
  export function managedSandboxHostName(backend) {
202
210
  return backendHostNames.get(backend);
203
211
  }
212
+ /**
213
+ * True when execute failed before the sandbox accepted the command.
214
+ *
215
+ * Matches only the cold-box readiness race (named error or message). Ambiguous
216
+ * stream/transport errors may have already started a command — those belong on
217
+ * other recreate matchers, not this classifier.
218
+ */
219
+ function isSandboxStreamError(error) {
220
+ if (error instanceof Error && STREAM_NEVER_STARTED_NAMES.has(error.name)) {
221
+ return true;
222
+ }
223
+ if (error !== null &&
224
+ typeof error === "object" &&
225
+ STREAM_NEVER_STARTED_NAMES.has(error.constructor?.name ?? "")) {
226
+ return true;
227
+ }
228
+ const message = error instanceof Error
229
+ ? `${error.name}: ${error.message}`
230
+ : typeof error === "string"
231
+ ? error
232
+ : String(error);
233
+ return STREAM_NEVER_STARTED_RE.test(message);
234
+ }
204
235
  /** True when a sandbox API error is worth invalidating + recreating once. */
205
236
  export function isTransientSandboxError(error) {
206
- // Never-started stream race is also recreate-safe (and retried in place first
207
- // by executeWithStreamRetry during provision). Truncated HTTP bodies and
208
- // peer-closed errors are recreate-only — do not replay in place.
237
+ // Never-started stream race is recreate-safe. Truncated HTTP bodies and
238
+ // peer-closed errors are recreate-only as well.
209
239
  if (isSandboxStreamError(error)) {
210
240
  return true;
211
241
  }
@@ -216,16 +246,6 @@ export function isTransientSandboxError(error) {
216
246
  : String(error);
217
247
  return /LangSmithSandboxError|HTTP 502|Bad Gateway|HTTP 503|HTTP 504|ECONNRESET|ETIMEDOUT|peer closed connection without sending complete message body|incomplete chunked read|sandbox.*(not found|gone|unavailable|terminated)/i.test(message);
218
248
  }
219
- /** Override the LangSmith SDK loader. Test-only. */
220
- export function setLangSmithSandboxClientLoaderForTests(loader) {
221
- langSmithSandboxClientLoader = loader ?? defaultLangSmithSandboxClientLoader;
222
- }
223
- /** Override the DeepAgents LangSmith backend wrapper. Test-only. */
224
- export function setLangSmithSandboxBackendForTests(backend) {
225
- langSmithSandboxBackendLoader = backend
226
- ? async () => backend
227
- : defaultLangSmithSandboxBackendLoader;
228
- }
229
249
  /**
230
250
  * Deployment this runtime belongs to, used to name the sandboxes it creates so
231
251
  * `mda delete` can find them again.
@@ -239,35 +259,40 @@ export function managedDeploymentName() {
239
259
  stringOption(process.env.MDA_DEPLOYMENT_NAME));
240
260
  }
241
261
  /**
242
- * Deterministic sandbox name for a deployment, sandbox scope, and provisioning
243
- * recipe.
262
+ * Deterministic sandbox name for a deployment and thread.
244
263
  *
245
- * The inputs are digested rather than embedded because the scope key carries
246
- * thread and assistant UUIDs; a short digest keeps the name clear of any length
247
- * limit while staying stable across container restarts, which is what lets a
248
- * restarted deployment re-adopt its sandbox instead of leaking it.
249
- *
250
- * Provisioning is part of the identity so that editing `setup.sh` or switching
251
- * snapshots produces a different name. An adopted sandbox skips provisioning, so
252
- * without this a redeploy would silently leave existing threads on the old
253
- * environment.
264
+ * The thread key is digested rather than embedded so names stay short and
265
+ * stable across container restarts. Recipe (`setup.sh` / bake base) changes do
266
+ * **not** rename live boxes new threads clone the new recipe; live threads
267
+ * keep `/workspace` until idle reclaim.
254
268
  */
255
- export function managedSandboxName(deployment, scopeKey, provisioning = {}) {
269
+ export function managedSandboxName(deployment, scopeKey) {
270
+ return managedSandboxNameWithDeploymentLimit(deployment, scopeKey, MAX_PRIMARY_DEPLOYMENT_COMPONENT_LENGTH);
271
+ }
272
+ function managedSandboxFallbackName(deployment, scopeKey) {
273
+ return `${managedSandboxNameWithDeploymentLimit(deployment, scopeKey, MAX_FALLBACK_DEPLOYMENT_COMPONENT_LENGTH)}-${sandboxNameSuffix()}`;
274
+ }
275
+ function managedSandboxNameWithDeploymentLimit(deployment, scopeKey, deploymentLimit) {
256
276
  const digest = createHash("sha256")
257
277
  .update(scopeKey)
258
- .update("\0")
259
- .update(provisioningFingerprint(provisioning))
260
278
  .digest("hex")
261
279
  .slice(0, SANDBOX_DIGEST_LENGTH);
262
- return `${deployment}${SANDBOX_NAME_SEPARATOR}${digest}`;
280
+ return `${sandboxDeploymentComponent(deployment, deploymentLimit)}${SANDBOX_NAME_SEPARATOR}${digest}`;
263
281
  }
264
- /** Stable string for a provisioning recipe, insensitive to option ordering. */
265
- function provisioningFingerprint(provisioning) {
266
- const createOptions = Object.entries(provisioning.createOptions ?? {}).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
267
- return JSON.stringify([createOptions, provisioning.setupScript ?? ""]);
282
+ function sandboxDeploymentComponent(deployment, maxLength) {
283
+ const characters = Array.from(deployment);
284
+ if (characters.length <= maxLength) {
285
+ return deployment;
286
+ }
287
+ const digest = createHash("sha256")
288
+ .update(deployment)
289
+ .digest("hex")
290
+ .slice(0, SANDBOX_DEPLOYMENT_DIGEST_LENGTH);
291
+ const prefixLength = maxLength - SANDBOX_DEPLOYMENT_DIGEST_LENGTH - 1;
292
+ return `${characters.slice(0, prefixLength).join("")}-${digest}`;
268
293
  }
269
- async function createLangSmithSandbox(sandbox, setupScript, config, options, mode, scopeKey) {
270
- const { backend, adopted, hostName } = await createLangSmithSandboxBackend(sandbox, scopeKey, setupScript);
294
+ async function createLangSmithSandbox(sandbox, config, options, mode, scopeKey) {
295
+ const { backend, adopted, hostName } = await createLangSmithSandboxBackend(sandbox, scopeKey);
271
296
  await runConnectorSandboxHooks(options?.connectors, {
272
297
  backend,
273
298
  // An adopted sandbox already ran create-time provisioning in an earlier
@@ -276,46 +301,52 @@ async function createLangSmithSandbox(sandbox, setupScript, config, options, mod
276
301
  config,
277
302
  identity: options?.identity,
278
303
  });
279
- // Only a new sandbox needs `setup.sh`: an adopted one ran this exact script
280
- // already, since a changed script yields a different sandbox name.
281
- if (!adopted && setupScript && setupScript.trim().length > 0) {
282
- await runSetupScript(backend, setupScript);
283
- }
284
304
  return { backend, hostName };
285
305
  }
286
- function sandboxCreateOptions(sandbox) {
287
- const { scope: _scope, apiKey: _apiKey, defaultTimeout: _defaultTimeout, templateName, snapshotId, ...resolvedCreateOptions } = sandbox.options ?? {};
288
- const createSandboxOptions = resolvedCreateOptions;
289
- if (snapshotId && templateName) {
290
- throw new Error("snapshotId and templateName are mutually exclusive. Pass only one creation source.");
291
- }
292
- if (templateName) {
293
- createSandboxOptions.snapshotName = templateName;
294
- }
295
- if (snapshotId) {
296
- createSandboxOptions.snapshotId = snapshotId;
306
+ /**
307
+ * Options for `createSandbox`. Thread boxes clone the deploy-baked recipe when
308
+ * present; author `snapshotName` / `snapshotId` / `dockerImage` are bake bases
309
+ * only and are not passed here.
310
+ */
311
+ export function sandboxCreateOptions(sandbox) {
312
+ const { apiKey: _apiKey, defaultTimeout: _defaultTimeout, snapshotName: _snapshotName, snapshotId: _snapshotId, dockerImage: _dockerImage, registry: _registry, ...resolvedCreateOptions } = sandbox.options ?? {};
313
+ const createSandboxOptions = {
314
+ ...resolvedCreateOptions,
315
+ };
316
+ const recipe = stringOption(process.env[RECIPE_SNAPSHOT_ENV]);
317
+ if (recipe) {
318
+ createSandboxOptions.snapshotName = recipe;
297
319
  }
298
320
  return createSandboxOptions;
299
321
  }
300
- function hostSandboxNameFor(sandbox, config, setupScript) {
322
+ function hostSandboxNameFor(config) {
301
323
  const deployment = managedDeploymentName();
302
324
  if (!deployment) {
303
325
  return undefined;
304
326
  }
305
- return managedSandboxName(deployment, sandboxScopeKey(sandbox, config), {
306
- createOptions: sandboxCreateOptions(sandbox),
307
- setupScript,
308
- });
327
+ return managedSandboxName(deployment, sandboxScopeKey(config));
309
328
  }
310
- async function sandboxClientFor(sandbox) {
311
- const { SandboxClient } = await langSmithSandboxClientLoader();
329
+ /**
330
+ * Constructor options for the LangSmith sandbox client.
331
+ *
332
+ * Both the API key and the workspace scope come from the environment `mda`
333
+ * wrote, so every sandbox call lands in the workspace holding the baked recipe.
334
+ */
335
+ export function sandboxClientOptions(sandbox) {
312
336
  const apiKey = stringOption(sandbox.options?.apiKey) ??
313
337
  stringOption(process.env.LANGSMITH_API_KEY);
314
- return new SandboxClient(apiKey ? { apiKey } : undefined);
338
+ const workspaceId = stringOption(process.env[WORKSPACE_ID_ENV]);
339
+ if (!apiKey && !workspaceId) {
340
+ return undefined;
341
+ }
342
+ return {
343
+ ...(apiKey ? { apiKey } : {}),
344
+ ...(workspaceId ? { headers: { [TENANT_HEADER]: workspaceId } } : {}),
345
+ };
315
346
  }
316
347
  async function deleteHostSandbox(sandbox, name) {
317
348
  try {
318
- const client = await sandboxClientFor(sandbox);
349
+ const client = new SandboxClient(sandboxClientOptions(sandbox));
319
350
  if (typeof client.deleteSandbox !== "function") {
320
351
  return;
321
352
  }
@@ -329,29 +360,25 @@ async function deleteHostSandbox(sandbox, name) {
329
360
  console.warn(`[mda] sandbox invalidate: delete failed name=${JSON.stringify(name)} err=${label}`);
330
361
  }
331
362
  }
332
- async function createLangSmithSandboxBackend(sandbox, scopeKey, setupScript) {
333
- const { apiKey = process.env.LANGSMITH_API_KEY, defaultTimeout } = sandbox.options ?? {};
363
+ async function createLangSmithSandboxBackend(sandbox, scopeKey) {
364
+ const { defaultTimeout } = sandbox.options ?? {};
334
365
  const createSandboxOptions = sandboxCreateOptions(sandbox);
335
- const { SandboxClient } = await langSmithSandboxClientLoader();
336
- const sdkOptions = stringOption(apiKey);
337
- const client = new SandboxClient(sdkOptions ? { apiKey: sdkOptions } : undefined);
366
+ const client = new SandboxClient(sandboxClientOptions(sandbox));
338
367
  const deployment = managedDeploymentName();
339
368
  const sandboxName = deployment
340
- ? managedSandboxName(deployment, scopeKey, {
341
- createOptions: createSandboxOptions,
342
- setupScript,
343
- })
369
+ ? managedSandboxName(deployment, scopeKey)
370
+ : undefined;
371
+ const fallbackName = deployment
372
+ ? () => managedSandboxFallbackName(deployment, scopeKey)
344
373
  : undefined;
345
- const { sandbox: langSmithSandbox, adopted } = await resolveSandboxInstance(client, createSandboxOptions, sandboxName);
346
- const resolved = langSmithSandbox;
347
- const hostName = stringOption(resolved.name) ?? sandboxName;
374
+ const { sandbox: langSmithSandbox, adopted } = await resolveSandboxInstance(client, createSandboxOptions, sandboxName, fallbackName);
375
+ const hostName = stringOption(langSmithSandbox.name) ?? sandboxName;
348
376
  console.info(`[mda] sandbox resolve: ${adopted ? "adopted" : "created"} ` +
349
377
  `name=${JSON.stringify(hostName ?? null)} ` +
350
- `id=${JSON.stringify(resolved.id ?? null)} ` +
378
+ `id=${JSON.stringify(langSmithSandbox.id ?? null)} ` +
351
379
  `scope=${JSON.stringify(scopeKey)}`);
352
- const Backend = await langSmithSandboxBackendLoader();
353
380
  return {
354
- backend: new Backend({
381
+ backend: new LangSmithSandbox({
355
382
  sandbox: langSmithSandbox,
356
383
  defaultTimeout,
357
384
  }),
@@ -366,7 +393,7 @@ async function createLangSmithSandboxBackend(sandbox, scopeKey, setupScript) {
366
393
  * finds the sandbox its predecessor created instead of stranding it until the
367
394
  * idle TTL fires. When `name` is undefined (local dev) this is a plain create.
368
395
  */
369
- async function resolveSandboxInstance(client, createSandboxOptions, name) {
396
+ async function resolveSandboxInstance(client, createSandboxOptions, name, fallbackName) {
370
397
  if (!name) {
371
398
  return {
372
399
  sandbox: await client.createSandbox(createSandboxOptions),
@@ -403,7 +430,7 @@ async function resolveSandboxInstance(client, createSandboxOptions, name) {
403
430
  // Keep the deployment prefix so teardown still finds this sandbox.
404
431
  const created = await client.createSandbox({
405
432
  ...createSandboxOptions,
406
- name: `${name}-${sandboxNameSuffix()}`,
433
+ name: fallbackName?.() ?? `${name}-${sandboxNameSuffix()}`,
407
434
  });
408
435
  invalidatedSandboxNames.delete(name);
409
436
  return { sandbox: created, adopted: false };
@@ -426,9 +453,6 @@ async function adoptSandbox(client, name) {
426
453
  catch {
427
454
  return undefined;
428
455
  }
429
- if (existing === undefined || existing === null) {
430
- return undefined;
431
- }
432
456
  const status = stringOption(existing.status)?.toLowerCase();
433
457
  // No status reported: assume the SDK handed back a usable sandbox.
434
458
  if (status === undefined || ADOPTABLE_STATUS.has(status)) {
@@ -452,29 +476,13 @@ function isSandboxNameConflict(error) {
452
476
  function sandboxNameSuffix() {
453
477
  return randomBytes(4).toString("hex");
454
478
  }
455
- function threadSyncKey(sandbox, config) {
456
- const scope = sandbox.options?.scope ?? "thread";
457
- if (scope === "agent") {
458
- // Agent-scoped sandboxes are shared; sync once per thread that uses them.
459
- try {
460
- return `thread:${threadIdFromConfig(config)}`;
461
- }
462
- catch {
463
- return "thread:__missing__";
464
- }
465
- }
466
- return sandboxScopeKey(sandbox, config);
467
- }
468
479
  function threadIdFromConfig(config) {
469
480
  const threadId = configValue(config, "thread_id");
470
481
  if (threadId === undefined) {
471
- throw new Error('sandbox scope "thread" requires configurable.thread_id');
482
+ throw new Error("managed sandboxes require configurable.thread_id (one sandbox per thread)");
472
483
  }
473
484
  return threadId;
474
485
  }
475
- function assistantIdFromConfig(config) {
476
- return configValue(config, "assistant_id") ?? "agent";
477
- }
478
486
  function stringOption(value) {
479
487
  if (typeof value !== "string")
480
488
  return undefined;