@tanstack/ai-sandbox 0.2.4 → 0.3.1
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/esm/agents-file.js +53 -34
- package/dist/esm/agents-file.js.map +1 -1
- package/dist/esm/align.d.ts +121 -0
- package/dist/esm/align.js +197 -0
- package/dist/esm/align.js.map +1 -0
- package/dist/esm/approvals.js +63 -29
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/attach-preflight.d.ts +85 -0
- package/dist/esm/attach-preflight.js +189 -0
- package/dist/esm/attach-preflight.js.map +1 -0
- package/dist/esm/bootstrap.js +103 -117
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/bridge-events.js +96 -71
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/capabilities.d.ts +0 -5
- package/dist/esm/capabilities.js +32 -28
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/chunk-identity.d.ts +52 -0
- package/dist/esm/chunk-identity.js +102 -0
- package/dist/esm/chunk-identity.js.map +1 -0
- package/dist/esm/claim.d.ts +187 -0
- package/dist/esm/claim.js +349 -0
- package/dist/esm/claim.js.map +1 -0
- package/dist/esm/contracts.d.ts +13 -0
- package/dist/esm/driver.d.ts +83 -0
- package/dist/esm/driver.js +138 -0
- package/dist/esm/driver.js.map +1 -0
- package/dist/esm/durability.d.ts +263 -0
- package/dist/esm/durability.js +230 -0
- package/dist/esm/durability.js.map +1 -0
- package/dist/esm/errors.js +28 -24
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/file-diff.js +151 -135
- package/dist/esm/file-diff.js.map +1 -1
- package/dist/esm/git-exec.js +51 -62
- package/dist/esm/git-exec.js.map +1 -1
- package/dist/esm/harness-cwd.js +24 -19
- package/dist/esm/harness-cwd.js.map +1 -1
- package/dist/esm/index.d.ts +30 -8
- package/dist/esm/index.js +23 -91
- package/dist/esm/instance-store.d.ts +88 -0
- package/dist/esm/instance-store.js +67 -0
- package/dist/esm/instance-store.js.map +1 -0
- package/dist/esm/journal-bytes.d.ts +67 -0
- package/dist/esm/journal-bytes.js +110 -0
- package/dist/esm/journal-bytes.js.map +1 -0
- package/dist/esm/journal-reader.d.ts +66 -0
- package/dist/esm/journal-reader.js +228 -0
- package/dist/esm/journal-reader.js.map +1 -0
- package/dist/esm/journal-sweep.d.ts +113 -0
- package/dist/esm/journal-sweep.js +309 -0
- package/dist/esm/journal-sweep.js.map +1 -0
- package/dist/esm/journal.d.ts +542 -0
- package/dist/esm/journal.js +679 -0
- package/dist/esm/journal.js.map +1 -0
- package/dist/esm/key.js +36 -33
- package/dist/esm/key.js.map +1 -1
- package/dist/esm/middleware.d.ts +50 -2
- package/dist/esm/middleware.js +335 -208
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.js +75 -49
- package/dist/esm/ngrok.js.map +1 -1
- package/dist/esm/policy.js +43 -34
- package/dist/esm/policy.js.map +1 -1
- package/dist/esm/projection.js +16 -8
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/reap.d.ts +238 -0
- package/dist/esm/reap.js +355 -0
- package/dist/esm/reap.js.map +1 -0
- package/dist/esm/reclaim.d.ts +84 -0
- package/dist/esm/reclaim.js +106 -0
- package/dist/esm/reclaim.js.map +1 -0
- package/dist/esm/remote-tools.js +73 -62
- package/dist/esm/remote-tools.js.map +1 -1
- package/dist/esm/run.d.ts +93 -25
- package/dist/esm/run.js +274 -79
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runner.d.ts +119 -2
- package/dist/esm/runner.js +270 -51
- package/dist/esm/runner.js.map +1 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +139 -123
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/secrets.js +39 -47
- package/dist/esm/secrets.js.map +1 -1
- package/dist/esm/setup-plan.js +22 -14
- package/dist/esm/setup-plan.js.map +1 -1
- package/dist/esm/shell.d.ts +8 -0
- package/dist/esm/shell.js +197 -158
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.d.ts +16 -0
- package/dist/esm/testkit/conformance.js +97 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
- package/dist/esm/testkit/journal-conformance.d.ts +51 -0
- package/dist/esm/testkit/journal-conformance.js +378 -0
- package/dist/esm/testkit/journal-conformance.js.map +1 -0
- package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
- package/dist/esm/testkit/reaper-conformance.js +847 -0
- package/dist/esm/testkit/reaper-conformance.js.map +1 -0
- package/dist/esm/testkit/shell-spawn.d.ts +2 -0
- package/dist/esm/testkit/shell-spawn.js +60 -0
- package/dist/esm/testkit/shell-spawn.js.map +1 -0
- package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
- package/dist/esm/testkit/takeover-conformance.js +685 -0
- package/dist/esm/testkit/takeover-conformance.js.map +1 -0
- package/dist/esm/tool-bridge.js +227 -180
- package/dist/esm/tool-bridge.js.map +1 -1
- package/dist/esm/tool-history.d.ts +62 -0
- package/dist/esm/tool-history.js +171 -0
- package/dist/esm/tool-history.js.map +1 -0
- package/dist/esm/watch.js +310 -236
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workspace.d.ts +1 -1
- package/dist/esm/workspace.js +49 -28
- package/dist/esm/workspace.js.map +1 -1
- package/package.json +16 -6
- package/skills/ai-sandbox/SKILL.md +658 -20
- package/src/align.ts +297 -0
- package/src/attach-preflight.ts +292 -0
- package/src/capabilities.ts +4 -13
- package/src/chunk-identity.ts +154 -0
- package/src/claim.ts +479 -0
- package/src/contracts.ts +13 -0
- package/src/driver.ts +205 -0
- package/src/durability.ts +380 -0
- package/src/index.ts +212 -27
- package/src/instance-store.ts +122 -0
- package/src/journal-bytes.ts +136 -0
- package/src/journal-reader.ts +359 -0
- package/src/journal-sweep.ts +406 -0
- package/src/journal.ts +875 -0
- package/src/middleware.ts +470 -30
- package/src/reap.ts +723 -0
- package/src/reclaim.ts +191 -0
- package/src/run.ts +365 -75
- package/src/runner.ts +347 -3
- package/src/sandbox.ts +38 -8
- package/src/shell.ts +106 -38
- package/src/testkit/conformance.ts +117 -0
- package/src/testkit/durable-run-fields-conformance.ts +147 -0
- package/src/testkit/journal-conformance.ts +676 -0
- package/src/testkit/reaper-conformance.ts +1201 -0
- package/src/testkit/shell-spawn.ts +67 -0
- package/src/testkit/takeover-conformance.ts +1040 -0
- package/src/tool-history.ts +245 -0
- package/src/workspace.ts +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/run-log.d.ts +0 -81
- package/dist/esm/run-log.js +0 -107
- package/dist/esm/run-log.js.map +0 -1
- package/dist/esm/store.d.ts +0 -53
- package/dist/esm/store.js +0 -34
- package/dist/esm/store.js.map +0 -1
- package/src/run-log.ts +0 -224
- package/src/store.ts +0 -83
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import "./journal.js";
|
|
2
|
+
import { alignToStoredLog, isBridgeCustomChunk } from "./align.js";
|
|
3
|
+
import { createCapability } from "@tanstack/ai";
|
|
4
|
+
//#region src/durability.ts
|
|
5
|
+
/**
|
|
6
|
+
* The durability seam for a sandboxed run: the option shape `withSandbox` takes,
|
|
7
|
+
* the capability harness adapters read, and the two guards that keep a
|
|
8
|
+
* "durable" run actually recoverable.
|
|
9
|
+
*
|
|
10
|
+
* A run is durable only when BOTH a `RunStore` and a `StreamDurability` are
|
|
11
|
+
* wired, because either alone is useless: a record with no event log cannot be
|
|
12
|
+
* replayed, and a log with no record cannot be found, claimed, or reaped. So the
|
|
13
|
+
* capability exists or it does not — there is no half-configured state, and
|
|
14
|
+
* every existing app (which wires neither) keeps today's behavior untouched.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Provided by `withSandbox` only when a run is genuinely durable (both stores
|
|
18
|
+
* wired). Harness adapters read it with `getOptional` and treat its absence as
|
|
19
|
+
* "no journaling contract to honour", which is exactly today's behavior.
|
|
20
|
+
*/
|
|
21
|
+
var SandboxDurabilityCapability = createCapability()("sandbox-durability");
|
|
22
|
+
/** Destructured accessors, matching `./capabilities`. */
|
|
23
|
+
var [getSandboxDurability, provideSandboxDurability] = SandboxDurabilityCapability;
|
|
24
|
+
/**
|
|
25
|
+
* A durable run was started without a caller-supplied `runId`.
|
|
26
|
+
*
|
|
27
|
+
* Thrown rather than defaulted because the failure is otherwise INVISIBLE: an
|
|
28
|
+
* adapter-generated id (`${name}-${Date.now()}-${Math.random()...}`) produces a
|
|
29
|
+
* journal path at `/tmp/tanstack-runs/<id>.ndjson` that no successor host can
|
|
30
|
+
* recompute, so the run streams normally, records normally, and is silently
|
|
31
|
+
* unrecoverable. A loud failure at the start of `chatStream` is strictly better
|
|
32
|
+
* than a run that only reveals itself as non-durable during an incident.
|
|
33
|
+
*/
|
|
34
|
+
var DurableRunIdRequiredError = class extends Error {
|
|
35
|
+
adapter;
|
|
36
|
+
constructor(adapter) {
|
|
37
|
+
super(`${adapter}: a durable sandboxed run requires a caller-supplied \`runId\`. The journal path and the deterministic message-id generator are both derived from it, so a successor host can only resume a run whose \`runId\` it can recompute. Pass \`runId\` to chat({ ... }), or drop \`runs\`/\`durability\` from withSandbox(...) to run non-durably.`);
|
|
38
|
+
this.adapter = adapter;
|
|
39
|
+
this.name = "DurableRunIdRequiredError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the `runId` a harness adapter will journal under.
|
|
44
|
+
*
|
|
45
|
+
* Replaces the bare `options.runId ?? this.generateId()` in every harness
|
|
46
|
+
* adapter. The fallback is preserved for non-durable runs — several `chat()`
|
|
47
|
+
* paths pass `runId` as a conditional spread, so `undefined` is reachable and
|
|
48
|
+
* removing the fallback would break them for no benefit.
|
|
49
|
+
*
|
|
50
|
+
* The `durable` check runs BEFORE `fallback()`, and that ordering is load
|
|
51
|
+
* bearing: a generated id must never be minted for a durable run, not even one
|
|
52
|
+
* that is discarded, because the whole point is that no such id can exist.
|
|
53
|
+
*/
|
|
54
|
+
function resolveDurableRunId(runId, options) {
|
|
55
|
+
if (runId !== void 0 && runId.length > 0) return runId;
|
|
56
|
+
if (options.durable) throw new DurableRunIdRequiredError(options.adapter);
|
|
57
|
+
return options.fallback();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* An ATTACHING durable run was driven without the run record's `threadId`.
|
|
61
|
+
*
|
|
62
|
+
* The sibling of {@link DurableRunIdRequiredError}, for the other id an attach
|
|
63
|
+
* cannot mint for itself. `threadId` lands in EVERY chunk a harness adapter
|
|
64
|
+
* emits (see each package's `stream/translate.ts`), so a replay that generates a
|
|
65
|
+
* fresh one produces a stream that differs from the stored log in its very first
|
|
66
|
+
* chunk. `alignToStoredLog` then fails at index 0 with a
|
|
67
|
+
* `JournalReplayThreadIdMismatchError` — mid-stream, after the takeover has
|
|
68
|
+
* already claimed the run. Refusing up front is strictly better, and mirrors
|
|
69
|
+
* what `resolveDurableRunId` does for an id whose absence is equally fatal.
|
|
70
|
+
*
|
|
71
|
+
* Core already does its part: `startRunDriver` reads the record and hands
|
|
72
|
+
* `active.threadId` to `drive({ runId, threadId, signal })`. This error exists
|
|
73
|
+
* for the one gap it cannot close — application `drive` code that forgets to
|
|
74
|
+
* forward it into `chat()`.
|
|
75
|
+
*/
|
|
76
|
+
var DurableThreadIdRequiredError = class extends Error {
|
|
77
|
+
adapter;
|
|
78
|
+
constructor(adapter) {
|
|
79
|
+
super(`${adapter}: an ATTACHING durable sandboxed run requires the run record's \`threadId\`. Every emitted chunk carries \`threadId\`, so an attach that generates a fresh one replays a stream whose first chunk already differs from the stored log, and alignment fails at index 0 (\`JournalReplayThreadIdMismatchError\`) even though the agent behaved identically. Forward the run record's \`threadId\` — the one \`sandboxRunDriver\` passes to \`drive({ runId, threadId, signal })\` — into \`chat({ ... })\` on the attach route. A durable FRESH run needs none: that run is what establishes the \`threadId\`.`);
|
|
80
|
+
this.adapter = adapter;
|
|
81
|
+
this.name = "DurableThreadIdRequiredError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the `threadId` a harness adapter will stamp on every chunk.
|
|
86
|
+
*
|
|
87
|
+
* Replaces the bare `options.threadId ?? this.generateId()` in the journaling
|
|
88
|
+
* harness adapters. Only the durable-AND-attaching quadrant throws; the other
|
|
89
|
+
* three keep the generated fallback and are byte-identical to before:
|
|
90
|
+
*
|
|
91
|
+
* | durable | attaching | behavior |
|
|
92
|
+
* | ------- | --------- | --------------------------------------------------- |
|
|
93
|
+
* | no | no | fallback — a plain non-durable run |
|
|
94
|
+
* | no | yes | fallback — not reachable today, and harmless anyway |
|
|
95
|
+
* | yes | no | fallback — the FRESH run that ESTABLISHES the id |
|
|
96
|
+
* | yes | yes | throw {@link DurableThreadIdRequiredError} |
|
|
97
|
+
*
|
|
98
|
+
* The durable-fresh row is the load-bearing one. A fresh durable run legitimately
|
|
99
|
+
* mints its `threadId` (there is no record to reuse one from), so throwing on
|
|
100
|
+
* `durable` alone — the obvious over-simplification — would break every durable
|
|
101
|
+
* run that has ever worked. Only re-entering an existing run has an id it MUST
|
|
102
|
+
* reuse, which is exactly the condition `attach` already expresses.
|
|
103
|
+
*
|
|
104
|
+
* As in `resolveDurableRunId`, the guard runs BEFORE `fallback()`: a generated id
|
|
105
|
+
* must never be minted on this path, not even one that is then discarded.
|
|
106
|
+
*/
|
|
107
|
+
function resolveDurableThreadId(threadId, options) {
|
|
108
|
+
if (threadId !== void 0 && threadId.length > 0) return threadId;
|
|
109
|
+
if (options.durable && options.attaching) throw new DurableThreadIdRequiredError(options.adapter);
|
|
110
|
+
return options.fallback();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* An ATTACH was driven into a code path that can never replay a run.
|
|
114
|
+
*
|
|
115
|
+
* The third sibling of {@link DurableRunIdRequiredError} and
|
|
116
|
+
* {@link DurableThreadIdRequiredError}, and the one that is not about a missing
|
|
117
|
+
* id: here every id is present and the path itself is the problem.
|
|
118
|
+
*
|
|
119
|
+
* `sandboxRunDriver`'s `drive()` re-invokes `chat()` with `attach: true`. On a
|
|
120
|
+
* JOURNALING path that is genuinely a replay — `spawnNdjson` tails the journal
|
|
121
|
+
* the previous host wrote, `awaitAttachableJournal` refuses a hopeless attach up
|
|
122
|
+
* front, and `alignedIfAttaching` suppresses the prefix already delivered. A
|
|
123
|
+
* protocol path with none of those three has no journal to tail and nothing to
|
|
124
|
+
* align against, so `attach: true` does not resume anything: it starts the agent
|
|
125
|
+
* over from scratch against the workspace the first attempt already mutated, and
|
|
126
|
+
* appends its entire output to a log that still holds the first attempt's.
|
|
127
|
+
*
|
|
128
|
+
* Deliberately NOT a `JournalAttachUnavailableError`. That error means "a
|
|
129
|
+
* journal that should exist has not appeared yet" — retryable, scoped to a wait
|
|
130
|
+
* (`attachWaitMs`). This condition is categorically different: the path cannot
|
|
131
|
+
* attach AT ALL, so telling a caller to wait would point it at something that is
|
|
132
|
+
* never coming. A 5xx/501-shaped refusal, not a 504.
|
|
133
|
+
*
|
|
134
|
+
* `reason` names the missing capability in the adapter's own vocabulary (which
|
|
135
|
+
* protocol, which spawn path), because the fix is always to change how the run
|
|
136
|
+
* is spawned or routed, never to retry.
|
|
137
|
+
*/
|
|
138
|
+
var DurableAttachNotSupportedError = class extends Error {
|
|
139
|
+
adapter;
|
|
140
|
+
reason;
|
|
141
|
+
constructor(adapter, reason) {
|
|
142
|
+
super(`${adapter}: this code path cannot ATTACH to an existing durable run (${reason}). It does not journal, so there is no stored output to replay and no alignment to suppress what was already delivered. Proceeding would re-run the agent from scratch against the workspace the previous attempt already modified, and double-append its entire output to the run log. Route the attach through a journaling spawn path, or drop \`runs\`/\`durability\` from withSandbox(...) so the run is never resumed in the first place. This is not a transient condition — unlike \`JournalAttachUnavailableError\`, waiting and retrying can never make it succeed.`);
|
|
143
|
+
this.adapter = adapter;
|
|
144
|
+
this.reason = reason;
|
|
145
|
+
this.name = "DurableAttachNotSupportedError";
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Resolve `withSandbox`'s two durability options into the capability payload, or
|
|
150
|
+
* `undefined` when the app has not opted in.
|
|
151
|
+
*
|
|
152
|
+
* BOTH `runs` and `durability` are required. A half-configured app gets
|
|
153
|
+
* `undefined` **silently** rather than a warning: it has not asked for
|
|
154
|
+
* durability, so there is nothing to warn about, and the resulting behavior
|
|
155
|
+
* (destroy on disconnect, no journal) is exactly today's.
|
|
156
|
+
*/
|
|
157
|
+
function resolveSandboxDurability(options) {
|
|
158
|
+
const runs = options?.runs;
|
|
159
|
+
const durability = options?.durability;
|
|
160
|
+
if (runs === void 0 || durability === void 0) return void 0;
|
|
161
|
+
return {
|
|
162
|
+
runs,
|
|
163
|
+
adapter: durability.adapter,
|
|
164
|
+
journalDir: durability.journal ?? "/tmp/tanstack-runs",
|
|
165
|
+
attach: durability.attach === true,
|
|
166
|
+
detachOnDisconnect: durability.detachOnDisconnect !== false,
|
|
167
|
+
...durability.pollIntervalMs === void 0 ? {} : { pollIntervalMs: durability.pollIntervalMs },
|
|
168
|
+
...durability.attachWaitMs === void 0 ? {} : { attachWaitMs: durability.attachWaitMs }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build the `spawnNdjson` journal option for a run, or `undefined` when the run
|
|
173
|
+
* is not durable — in which case `spawnNdjson` takes its original, unjournaled
|
|
174
|
+
* path (`isJournaled` tests `options.journal !== undefined`, `runner.ts:70-72`)
|
|
175
|
+
* and behavior is byte-identical to a pre-durability run.
|
|
176
|
+
*
|
|
177
|
+
* `JournalOptions.dir` is optional, but this always supplies it: the resolved
|
|
178
|
+
* durability has already defaulted `journalDir`, and a successor host must
|
|
179
|
+
* recompute the same path rather than re-derive the default independently.
|
|
180
|
+
*
|
|
181
|
+
* `runs` and `attachWaitMs` are carried ONLY when attaching, and that is not a
|
|
182
|
+
* micro-optimization: they exist for `awaitAttachableJournal`, which the reader
|
|
183
|
+
* runs on the attach path alone. A fresh run has no journal yet BY DESIGN (its own
|
|
184
|
+
* `journaledCommand` spawn creates it moments later), so handing it a run store
|
|
185
|
+
* would only invite a future change to gate a path where absence proves nothing.
|
|
186
|
+
*/
|
|
187
|
+
function journalOptionsFor(durability, runId) {
|
|
188
|
+
if (durability === void 0) return void 0;
|
|
189
|
+
return {
|
|
190
|
+
runId,
|
|
191
|
+
dir: durability.journalDir,
|
|
192
|
+
attach: durability.attach,
|
|
193
|
+
...durability.pollIntervalMs === void 0 ? {} : { pollIntervalMs: durability.pollIntervalMs },
|
|
194
|
+
...durability.attach ? {
|
|
195
|
+
runs: durability.runs,
|
|
196
|
+
...durability.attachWaitMs === void 0 ? {} : { attachWaitMs: durability.attachWaitMs }
|
|
197
|
+
} : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Align a harness stream against the run's stored log — but ONLY on an attach.
|
|
202
|
+
*
|
|
203
|
+
* The `attach` guard is not an optimization, it is a CORRECTNESS requirement.
|
|
204
|
+
* `alignToStoredLog` snapshots the log before the first chunk is pulled and
|
|
205
|
+
* treats everything in that snapshot as "already delivered". On a FRESH run that
|
|
206
|
+
* premise is false: if such a run were aligned against a log that already holds
|
|
207
|
+
* entries — a `runId` collision, a retried request — its own chunks would be
|
|
208
|
+
* matched against those entries and silently SUPPRESSED instead of delivered,
|
|
209
|
+
* which is silent data loss rather than a slow path. Aligning only when
|
|
210
|
+
* re-entering an existing run keeps the transform's premise ("this stream is a
|
|
211
|
+
* replay of what is already stored") actually true.
|
|
212
|
+
*
|
|
213
|
+
* `isBridgeCustomChunk` is passed because the stored log holds the previous
|
|
214
|
+
* host's MERGED output, including live bridged-tool CUSTOM events that a replay
|
|
215
|
+
* cannot reproduce; without it a bridged-tool run could not be taken over at
|
|
216
|
+
* all. Wrap the merge RESULT, never the pre-merge translator, or the comparison
|
|
217
|
+
* is against a stream the log never contained.
|
|
218
|
+
*/
|
|
219
|
+
function alignedIfAttaching(chunks, durability, logger) {
|
|
220
|
+
if (durability === void 0 || !durability.attach) return chunks;
|
|
221
|
+
return alignToStoredLog(chunks, {
|
|
222
|
+
durability: durability.adapter,
|
|
223
|
+
isOutOfBand: isBridgeCustomChunk,
|
|
224
|
+
...logger === void 0 ? {} : { logger }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
export { DurableAttachNotSupportedError, DurableRunIdRequiredError, DurableThreadIdRequiredError, SandboxDurabilityCapability, alignedIfAttaching, getSandboxDurability, journalOptionsFor, provideSandboxDurability, resolveDurableRunId, resolveDurableThreadId, resolveSandboxDurability };
|
|
229
|
+
|
|
230
|
+
//# sourceMappingURL=durability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durability.js","names":[],"sources":["../../src/durability.ts"],"sourcesContent":["/**\n * The durability seam for a sandboxed run: the option shape `withSandbox` takes,\n * the capability harness adapters read, and the two guards that keep a\n * \"durable\" run actually recoverable.\n *\n * A run is durable only when BOTH a `RunStore` and a `StreamDurability` are\n * wired, because either alone is useless: a record with no event log cannot be\n * replayed, and a log with no record cannot be found, claimed, or reaped. So the\n * capability exists or it does not — there is no half-configured state, and\n * every existing app (which wires neither) keeps today's behavior untouched.\n */\nimport { createCapability } from '@tanstack/ai'\nimport { DEFAULT_JOURNAL_DIR } from './journal'\nimport { alignToStoredLog, isBridgeCustomChunk } from './align'\nimport type { JournalOptions } from './runner'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { RunStore, StreamChunk, StreamDurability } from '@tanstack/ai'\n\n/** `withSandbox(sandbox, { durability })`. */\nexport interface SandboxDurabilityOptions<TOffset extends string = string> {\n /**\n * Delivery-durable event log for the run. Same key and shape as the\n * transport's `durability.adapter`, so one adapter instance can be handed to\n * both `withSandbox` and `toServerSentEventsResponse`.\n *\n * Generic in the offset type, defaulted to `string`, for the same reason\n * {@link SandboxRunDriverOptions} and {@link ReapOptions} are:\n * `StreamDurability` is INVARIANT in `TOffset` (`read` takes an offset in),\n * so a backend that brands its cursors — `@tanstack/ai-durable-stream`'s\n * `durableStream`, the multi-host production backend the sandbox docs point\n * at — is not assignable to `StreamDurability<string>`. Without the parameter\n * the resume route could be wired with it and the route that STARTS the run\n * could not.\n */\n adapter: StreamDurability<TOffset>\n /** Journal directory inside the sandbox. Defaults to `/tmp/tanstack-runs`. */\n journal?: string\n /**\n * Whether a client disconnect DETACHES (leave the agent running) instead of\n * destroying the sandbox. Defaults to `true` whenever durability is wired,\n * because that is the whole point of wiring it.\n *\n * Set `false` to keep today's destroy-on-disconnect cost profile while still\n * getting resumable DELIVERY (a reload replays the log). An explicit cancel\n * destroys either way.\n */\n detachOnDisconnect?: boolean\n /**\n * Read an EXISTING run's journal instead of starting a new agent. Set by the\n * attach route's `drive()` callback, never by an application's POST handler.\n *\n * This is where `attach` lives, and deliberately NOT on `chat()`: `chat()` is\n * core and must not gain sandbox vocabulary, and the provider options are\n * per-model type state, not per-request lifecycle.\n */\n attach?: boolean\n /** Journal poll interval for providers that cannot follow. */\n pollIntervalMs?: number\n /**\n * How long an ATTACH waits for a live run's journal to appear before failing\n * with a `JournalAttachUnavailableError`. Defaults to\n * `DEFAULT_ATTACH_JOURNAL_WAIT_MS` (10s). Only the wait is configurable: an\n * unknown or terminal runId fails immediately regardless, since no amount of\n * waiting changes either verdict.\n */\n attachWaitMs?: number\n}\n\n/**\n * The view of a caller's event log that the capability bus carries.\n *\n * Deliberately NOT the whole `StreamDurability`. `read` is the only member that\n * takes an offset *in*, which is what makes `StreamDurability` invariant in\n * `TOffset` and a branded-cursor backend unassignable to\n * `StreamDurability<string>`. Every other member mentions the offset only in a\n * return position, so this type is a genuine SUPERTYPE of\n * `StreamDurability<TOffset>` for every `TOffset extends string` — which is the\n * one property that lets a single concrete capability instantiation accept a\n * branded backend. `createCapability<T>()` forces exactly one instantiation\n * (the value type is a plain type argument, and TypeScript has no higher-kinded\n * types), so the payload cannot be parameterized the way the *option* above is.\n *\n * Dropping `read` costs nothing, and that is a property of the seam rather than\n * luck: the bus is the JOURNAL/ALIGNMENT seam, and alignment reads the stored\n * prefix through `snapshot()` — never `read()`, which tails an open log forever\n * (see `alignToStoredLog`). Replay *by offset* belongs to the delivery seam,\n * and that seam (`toServerSentEventsResponse`, `sandboxRunDriver`) receives the\n * application's own adapter directly, with its brand intact.\n */\nexport type SandboxDurabilityLog = Omit<StreamDurability, 'read'>\n\n/**\n * Resolved durability, published on the capability bus by `withSandbox`.\n *\n * Deliberately carries NO detached-run TTL. The only actor that enforces one is\n * `reapDetachedRuns`, which runs from a cron with no chat in flight — so it has\n * no `CapabilityContext` and cannot read this bus at all. A TTL published here\n * could therefore only ever be read by nobody, while the sweep took its own\n * `ReapOptions.detachedRunTtlMs`; the two would silently disagree. The reaper's\n * required option is the single source of truth.\n */\nexport interface SandboxRunDurability {\n runs: RunStore\n adapter: SandboxDurabilityLog\n journalDir: string\n attach: boolean\n detachOnDisconnect: boolean\n pollIntervalMs?: number\n attachWaitMs?: number\n}\n\n/**\n * Provided by `withSandbox` only when a run is genuinely durable (both stores\n * wired). Harness adapters read it with `getOptional` and treat its absence as\n * \"no journaling contract to honour\", which is exactly today's behavior.\n */\nexport const SandboxDurabilityCapability =\n createCapability<SandboxRunDurability>()('sandbox-durability')\n\n/** Destructured accessors, matching `./capabilities`. */\nexport const [getSandboxDurability, provideSandboxDurability] =\n SandboxDurabilityCapability\n\n/**\n * A durable run was started without a caller-supplied `runId`.\n *\n * Thrown rather than defaulted because the failure is otherwise INVISIBLE: an\n * adapter-generated id (`${name}-${Date.now()}-${Math.random()...}`) produces a\n * journal path at `/tmp/tanstack-runs/<id>.ndjson` that no successor host can\n * recompute, so the run streams normally, records normally, and is silently\n * unrecoverable. A loud failure at the start of `chatStream` is strictly better\n * than a run that only reveals itself as non-durable during an incident.\n */\nexport class DurableRunIdRequiredError extends Error {\n constructor(readonly adapter: string) {\n super(\n `${adapter}: a durable sandboxed run requires a caller-supplied \\`runId\\`. ` +\n `The journal path and the deterministic message-id generator are both derived from it, ` +\n `so a successor host can only resume a run whose \\`runId\\` it can recompute. ` +\n `Pass \\`runId\\` to chat({ ... }), or drop \\`runs\\`/\\`durability\\` from withSandbox(...) to run non-durably.`,\n )\n this.name = 'DurableRunIdRequiredError'\n }\n}\n\n/**\n * Resolve the `runId` a harness adapter will journal under.\n *\n * Replaces the bare `options.runId ?? this.generateId()` in every harness\n * adapter. The fallback is preserved for non-durable runs — several `chat()`\n * paths pass `runId` as a conditional spread, so `undefined` is reachable and\n * removing the fallback would break them for no benefit.\n *\n * The `durable` check runs BEFORE `fallback()`, and that ordering is load\n * bearing: a generated id must never be minted for a durable run, not even one\n * that is discarded, because the whole point is that no such id can exist.\n */\nexport function resolveDurableRunId(\n runId: string | undefined,\n options: { durable: boolean; adapter: string; fallback: () => string },\n): string {\n if (runId !== undefined && runId.length > 0) return runId\n if (options.durable) throw new DurableRunIdRequiredError(options.adapter)\n return options.fallback()\n}\n\n/**\n * An ATTACHING durable run was driven without the run record's `threadId`.\n *\n * The sibling of {@link DurableRunIdRequiredError}, for the other id an attach\n * cannot mint for itself. `threadId` lands in EVERY chunk a harness adapter\n * emits (see each package's `stream/translate.ts`), so a replay that generates a\n * fresh one produces a stream that differs from the stored log in its very first\n * chunk. `alignToStoredLog` then fails at index 0 with a\n * `JournalReplayThreadIdMismatchError` — mid-stream, after the takeover has\n * already claimed the run. Refusing up front is strictly better, and mirrors\n * what `resolveDurableRunId` does for an id whose absence is equally fatal.\n *\n * Core already does its part: `startRunDriver` reads the record and hands\n * `active.threadId` to `drive({ runId, threadId, signal })`. This error exists\n * for the one gap it cannot close — application `drive` code that forgets to\n * forward it into `chat()`.\n */\nexport class DurableThreadIdRequiredError extends Error {\n constructor(readonly adapter: string) {\n super(\n `${adapter}: an ATTACHING durable sandboxed run requires the run record's \\`threadId\\`. ` +\n `Every emitted chunk carries \\`threadId\\`, so an attach that generates a fresh one replays a stream whose first chunk ` +\n `already differs from the stored log, and alignment fails at index 0 (\\`JournalReplayThreadIdMismatchError\\`) even though ` +\n `the agent behaved identically. Forward the run record's \\`threadId\\` — the one \\`sandboxRunDriver\\` passes to ` +\n `\\`drive({ runId, threadId, signal })\\` — into \\`chat({ ... })\\` on the attach route. ` +\n `A durable FRESH run needs none: that run is what establishes the \\`threadId\\`.`,\n )\n this.name = 'DurableThreadIdRequiredError'\n }\n}\n\n/**\n * Resolve the `threadId` a harness adapter will stamp on every chunk.\n *\n * Replaces the bare `options.threadId ?? this.generateId()` in the journaling\n * harness adapters. Only the durable-AND-attaching quadrant throws; the other\n * three keep the generated fallback and are byte-identical to before:\n *\n * | durable | attaching | behavior |\n * | ------- | --------- | --------------------------------------------------- |\n * | no | no | fallback — a plain non-durable run |\n * | no | yes | fallback — not reachable today, and harmless anyway |\n * | yes | no | fallback — the FRESH run that ESTABLISHES the id |\n * | yes | yes | throw {@link DurableThreadIdRequiredError} |\n *\n * The durable-fresh row is the load-bearing one. A fresh durable run legitimately\n * mints its `threadId` (there is no record to reuse one from), so throwing on\n * `durable` alone — the obvious over-simplification — would break every durable\n * run that has ever worked. Only re-entering an existing run has an id it MUST\n * reuse, which is exactly the condition `attach` already expresses.\n *\n * As in `resolveDurableRunId`, the guard runs BEFORE `fallback()`: a generated id\n * must never be minted on this path, not even one that is then discarded.\n */\nexport function resolveDurableThreadId(\n threadId: string | undefined,\n options: {\n durable: boolean\n attaching: boolean\n adapter: string\n fallback: () => string\n },\n): string {\n if (threadId !== undefined && threadId.length > 0) return threadId\n if (options.durable && options.attaching) {\n throw new DurableThreadIdRequiredError(options.adapter)\n }\n return options.fallback()\n}\n\n/**\n * An ATTACH was driven into a code path that can never replay a run.\n *\n * The third sibling of {@link DurableRunIdRequiredError} and\n * {@link DurableThreadIdRequiredError}, and the one that is not about a missing\n * id: here every id is present and the path itself is the problem.\n *\n * `sandboxRunDriver`'s `drive()` re-invokes `chat()` with `attach: true`. On a\n * JOURNALING path that is genuinely a replay — `spawnNdjson` tails the journal\n * the previous host wrote, `awaitAttachableJournal` refuses a hopeless attach up\n * front, and `alignedIfAttaching` suppresses the prefix already delivered. A\n * protocol path with none of those three has no journal to tail and nothing to\n * align against, so `attach: true` does not resume anything: it starts the agent\n * over from scratch against the workspace the first attempt already mutated, and\n * appends its entire output to a log that still holds the first attempt's.\n *\n * Deliberately NOT a `JournalAttachUnavailableError`. That error means \"a\n * journal that should exist has not appeared yet\" — retryable, scoped to a wait\n * (`attachWaitMs`). This condition is categorically different: the path cannot\n * attach AT ALL, so telling a caller to wait would point it at something that is\n * never coming. A 5xx/501-shaped refusal, not a 504.\n *\n * `reason` names the missing capability in the adapter's own vocabulary (which\n * protocol, which spawn path), because the fix is always to change how the run\n * is spawned or routed, never to retry.\n */\nexport class DurableAttachNotSupportedError extends Error {\n constructor(\n readonly adapter: string,\n readonly reason: string,\n ) {\n super(\n `${adapter}: this code path cannot ATTACH to an existing durable run (${reason}). ` +\n `It does not journal, so there is no stored output to replay and no alignment to suppress what was already delivered. ` +\n `Proceeding would re-run the agent from scratch against the workspace the previous attempt already modified, and double-append its entire output to the run log. ` +\n `Route the attach through a journaling spawn path, or drop \\`runs\\`/\\`durability\\` from withSandbox(...) so the run is never resumed in the first place. ` +\n `This is not a transient condition — unlike \\`JournalAttachUnavailableError\\`, waiting and retrying can never make it succeed.`,\n )\n this.name = 'DurableAttachNotSupportedError'\n }\n}\n\n/**\n * Resolve `withSandbox`'s two durability options into the capability payload, or\n * `undefined` when the app has not opted in.\n *\n * BOTH `runs` and `durability` are required. A half-configured app gets\n * `undefined` **silently** rather than a warning: it has not asked for\n * durability, so there is nothing to warn about, and the resulting behavior\n * (destroy on disconnect, no journal) is exactly today's.\n */\nexport function resolveSandboxDurability<TOffset extends string = string>(\n options:\n | { runs?: RunStore; durability?: SandboxDurabilityOptions<TOffset> }\n | undefined,\n): SandboxRunDurability | undefined {\n const runs = options?.runs\n const durability = options?.durability\n if (runs === undefined || durability === undefined) return undefined\n return {\n runs,\n adapter: durability.adapter,\n journalDir: durability.journal ?? DEFAULT_JOURNAL_DIR,\n attach: durability.attach === true,\n detachOnDisconnect: durability.detachOnDisconnect !== false,\n ...(durability.pollIntervalMs === undefined\n ? {}\n : { pollIntervalMs: durability.pollIntervalMs }),\n ...(durability.attachWaitMs === undefined\n ? {}\n : { attachWaitMs: durability.attachWaitMs }),\n }\n}\n\n/**\n * Build the `spawnNdjson` journal option for a run, or `undefined` when the run\n * is not durable — in which case `spawnNdjson` takes its original, unjournaled\n * path (`isJournaled` tests `options.journal !== undefined`, `runner.ts:70-72`)\n * and behavior is byte-identical to a pre-durability run.\n *\n * `JournalOptions.dir` is optional, but this always supplies it: the resolved\n * durability has already defaulted `journalDir`, and a successor host must\n * recompute the same path rather than re-derive the default independently.\n *\n * `runs` and `attachWaitMs` are carried ONLY when attaching, and that is not a\n * micro-optimization: they exist for `awaitAttachableJournal`, which the reader\n * runs on the attach path alone. A fresh run has no journal yet BY DESIGN (its own\n * `journaledCommand` spawn creates it moments later), so handing it a run store\n * would only invite a future change to gate a path where absence proves nothing.\n */\nexport function journalOptionsFor(\n durability: SandboxRunDurability | undefined,\n runId: string,\n): JournalOptions | undefined {\n if (durability === undefined) return undefined\n return {\n runId,\n dir: durability.journalDir,\n attach: durability.attach,\n ...(durability.pollIntervalMs === undefined\n ? {}\n : { pollIntervalMs: durability.pollIntervalMs }),\n ...(durability.attach\n ? {\n runs: durability.runs,\n ...(durability.attachWaitMs === undefined\n ? {}\n : { attachWaitMs: durability.attachWaitMs }),\n }\n : {}),\n }\n}\n\n/**\n * Align a harness stream against the run's stored log — but ONLY on an attach.\n *\n * The `attach` guard is not an optimization, it is a CORRECTNESS requirement.\n * `alignToStoredLog` snapshots the log before the first chunk is pulled and\n * treats everything in that snapshot as \"already delivered\". On a FRESH run that\n * premise is false: if such a run were aligned against a log that already holds\n * entries — a `runId` collision, a retried request — its own chunks would be\n * matched against those entries and silently SUPPRESSED instead of delivered,\n * which is silent data loss rather than a slow path. Aligning only when\n * re-entering an existing run keeps the transform's premise (\"this stream is a\n * replay of what is already stored\") actually true.\n *\n * `isBridgeCustomChunk` is passed because the stored log holds the previous\n * host's MERGED output, including live bridged-tool CUSTOM events that a replay\n * cannot reproduce; without it a bridged-tool run could not be taken over at\n * all. Wrap the merge RESULT, never the pre-merge translator, or the comparison\n * is against a stream the log never contained.\n */\nexport function alignedIfAttaching(\n chunks: AsyncIterable<StreamChunk>,\n durability: SandboxRunDurability | undefined,\n logger?: InternalLogger,\n): AsyncIterable<StreamChunk> {\n if (durability === undefined || !durability.attach) return chunks\n return alignToStoredLog(chunks, {\n durability: durability.adapter,\n isOutOfBand: isBridgeCustomChunk,\n ...(logger === undefined ? {} : { logger }),\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoHA,IAAa,8BACX,iBAAuC,CAAC,CAAC,oBAAoB;;AAG/D,IAAa,CAAC,sBAAsB,4BAClC;;;;;;;;;;;AAYF,IAAa,4BAAb,cAA+C,MAAM;CAC9B;CAArB,YAAY,SAA0B;EACpC,MACE,GAAG,QAAQ,6UAIb;EANmB,KAAA,UAAA;EAOnB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;AAcA,SAAgB,oBACd,OACA,SACQ;CACR,IAAI,UAAU,KAAA,KAAa,MAAM,SAAS,GAAG,OAAO;CACpD,IAAI,QAAQ,SAAS,MAAM,IAAI,0BAA0B,QAAQ,OAAO;CACxE,OAAO,QAAQ,SAAS;AAC1B;;;;;;;;;;;;;;;;;;AAmBA,IAAa,+BAAb,cAAkD,MAAM;CACjC;CAArB,YAAY,SAA0B;EACpC,MACE,GAAG,QAAQ,6kBAMb;EARmB,KAAA,UAAA;EASnB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,uBACd,UACA,SAMQ;CACR,IAAI,aAAa,KAAA,KAAa,SAAS,SAAS,GAAG,OAAO;CAC1D,IAAI,QAAQ,WAAW,QAAQ,WAC7B,MAAM,IAAI,6BAA6B,QAAQ,OAAO;CAExD,OAAO,QAAQ,SAAS;AAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,iCAAb,cAAoD,MAAM;CAE7C;CACA;CAFX,YACE,SACA,QACA;EACA,MACE,GAAG,QAAQ,6DAA6D,OAAO,8iBAKjF;EATS,KAAA,UAAA;EACA,KAAA,SAAA;EAST,KAAK,OAAO;CACd;AACF;;;;;;;;;;AAWA,SAAgB,yBACd,SAGkC;CAClC,MAAM,OAAO,SAAS;CACtB,MAAM,aAAa,SAAS;CAC5B,IAAI,SAAS,KAAA,KAAa,eAAe,KAAA,GAAW,OAAO,KAAA;CAC3D,OAAO;EACL;EACA,SAAS,WAAW;EACpB,YAAY,WAAW,WAAA;EACvB,QAAQ,WAAW,WAAW;EAC9B,oBAAoB,WAAW,uBAAuB;EACtD,GAAI,WAAW,mBAAmB,KAAA,IAC9B,CAAC,IACD,EAAE,gBAAgB,WAAW,eAAe;EAChD,GAAI,WAAW,iBAAiB,KAAA,IAC5B,CAAC,IACD,EAAE,cAAc,WAAW,aAAa;CAC9C;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAgB,kBACd,YACA,OAC4B;CAC5B,IAAI,eAAe,KAAA,GAAW,OAAO,KAAA;CACrC,OAAO;EACL;EACA,KAAK,WAAW;EAChB,QAAQ,WAAW;EACnB,GAAI,WAAW,mBAAmB,KAAA,IAC9B,CAAC,IACD,EAAE,gBAAgB,WAAW,eAAe;EAChD,GAAI,WAAW,SACX;GACE,MAAM,WAAW;GACjB,GAAI,WAAW,iBAAiB,KAAA,IAC5B,CAAC,IACD,EAAE,cAAc,WAAW,aAAa;EAC9C,IACA,CAAC;CACP;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,mBACd,QACA,YACA,QAC4B;CAC5B,IAAI,eAAe,KAAA,KAAa,CAAC,WAAW,QAAQ,OAAO;CAC3D,OAAO,iBAAiB,QAAQ;EAC9B,YAAY,WAAW;EACvB,aAAa;EACb,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;CAC3C,CAAC;AACH"}
|
package/dist/esm/errors.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.name = "MissingSandboxError";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export {
|
|
22
|
-
MissingSandboxError,
|
|
23
|
-
UnsupportedCapabilityError
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when code invokes an optional sandbox capability that the active
|
|
4
|
+
* provider does not support. Core/middleware should check
|
|
5
|
+
* `handle.capabilities` BEFORE using an optional capability and degrade
|
|
6
|
+
* gracefully; this error exists so that a direct call to an unsupported
|
|
7
|
+
* optional method fails loud instead of silently no-opping.
|
|
8
|
+
*/
|
|
9
|
+
var UnsupportedCapabilityError = class extends Error {
|
|
10
|
+
provider;
|
|
11
|
+
capability;
|
|
12
|
+
constructor(provider, capability, hint) {
|
|
13
|
+
super(`Sandbox provider "${provider}" does not support the "${capability}" capability.` + (hint ? ` ${hint}` : ""));
|
|
14
|
+
this.name = "UnsupportedCapabilityError";
|
|
15
|
+
this.provider = provider;
|
|
16
|
+
this.capability = capability;
|
|
17
|
+
}
|
|
24
18
|
};
|
|
25
|
-
|
|
19
|
+
/** Thrown when a harness adapter requires a sandbox but none was provided. */
|
|
20
|
+
var MissingSandboxError = class extends Error {
|
|
21
|
+
constructor(adapterName) {
|
|
22
|
+
super(`Adapter "${adapterName}" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`);
|
|
23
|
+
this.name = "MissingSandboxError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { MissingSandboxError, UnsupportedCapabilityError };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/esm/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sources":["../../src/errors.ts"],"sourcesContent":["/**\n * Thrown when code invokes an optional sandbox capability that the active\n * provider does not support. Core/middleware should check\n * `handle.capabilities` BEFORE using an optional capability and degrade\n * gracefully; this error exists so that a direct call to an unsupported\n * optional method fails loud instead of silently no-opping.\n */\nexport class UnsupportedCapabilityError extends Error {\n readonly provider: string\n readonly capability: string\n\n constructor(provider: string, capability: string, hint?: string) {\n super(\n `Sandbox provider \"${provider}\" does not support the \"${capability}\" capability.` +\n (hint ? ` ${hint}` : ''),\n )\n this.name = 'UnsupportedCapabilityError'\n this.provider = provider\n this.capability = capability\n }\n}\n\n/** Thrown when a harness adapter requires a sandbox but none was provided. */\nexport class MissingSandboxError extends Error {\n constructor(adapterName: string) {\n super(\n `Adapter \"${adapterName}\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`,\n )\n this.name = 'MissingSandboxError'\n }\n}\n"],"
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/errors.ts"],"sourcesContent":["/**\n * Thrown when code invokes an optional sandbox capability that the active\n * provider does not support. Core/middleware should check\n * `handle.capabilities` BEFORE using an optional capability and degrade\n * gracefully; this error exists so that a direct call to an unsupported\n * optional method fails loud instead of silently no-opping.\n */\nexport class UnsupportedCapabilityError extends Error {\n readonly provider: string\n readonly capability: string\n\n constructor(provider: string, capability: string, hint?: string) {\n super(\n `Sandbox provider \"${provider}\" does not support the \"${capability}\" capability.` +\n (hint ? ` ${hint}` : ''),\n )\n this.name = 'UnsupportedCapabilityError'\n this.provider = provider\n this.capability = capability\n }\n}\n\n/** Thrown when a harness adapter requires a sandbox but none was provided. */\nexport class MissingSandboxError extends Error {\n constructor(adapterName: string) {\n super(\n `Adapter \"${adapterName}\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`,\n )\n this.name = 'MissingSandboxError'\n }\n}\n"],"mappings":";;;;;;;;AAOA,IAAa,6BAAb,cAAgD,MAAM;CACpD;CACA;CAEA,YAAY,UAAkB,YAAoB,MAAe;EAC/D,MACE,qBAAqB,SAAS,0BAA0B,WAAW,kBAChE,OAAO,IAAI,SAAS,GACzB;EACA,KAAK,OAAO;EACZ,KAAK,WAAW;EAChB,KAAK,aAAa;CACpB;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,aAAqB;EAC/B,MACE,YAAY,YAAY,oFAC1B;EACA,KAAK,OAAO;CACd;AACF"}
|
package/dist/esm/file-diff.js
CHANGED
|
@@ -1,145 +1,161 @@
|
|
|
1
|
+
//#region src/file-diff.ts
|
|
2
|
+
/** Path relative to the repo/workspace root, POSIX form. */
|
|
1
3
|
function relTo(root, path) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
const prefix = root.endsWith("/") ? root : `${root}/`;
|
|
5
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
4
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* POSIX single-quote escape for embedding a value in a shell command.
|
|
9
|
+
*/
|
|
5
10
|
function q(value) {
|
|
6
|
-
|
|
11
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Unified add-patch for a brand-new file, closely following the shape `git
|
|
15
|
+
* diff` produces for an added file (`diff --git` header + `new file mode` +
|
|
16
|
+
* `--- /dev/null` + `+++ b/<rel>`), so synthesized `create` diffs align with
|
|
17
|
+
* the real `git diff` output emitted for `change` events. `rel` must be the
|
|
18
|
+
* repo-root-relative POSIX path (like git's). Reproduces git's `\ No newline
|
|
19
|
+
* at end of file` marker and the header-only form for a zero-byte file, so a
|
|
20
|
+
* consumer applying the patch reconstructs the file byte-for-byte. It is not
|
|
21
|
+
* byte-identical to git — it omits the `index <hash>..<hash>` line and always
|
|
22
|
+
* writes the `+1,N` hunk count (git omits `,1`) — but both are valid
|
|
23
|
+
* unified-diff and accepted by `git apply`/`patch`.
|
|
24
|
+
*/
|
|
8
25
|
function synthesizeAddPatch(rel, content) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const body = lines.map((l) => `+${l}`).join("\n");
|
|
16
|
-
return header + `--- /dev/null
|
|
17
|
-
+++ b/${rel}
|
|
18
|
-
@@ -0,0 +1,${lines.length} @@
|
|
19
|
-
` + body + (hasFinalNewline ? "\n" : "\n\\n");
|
|
26
|
+
const header = `diff --git a/${rel} b/${rel}\nnew file mode 100644\n`;
|
|
27
|
+
if (content === "") return header;
|
|
28
|
+
const hasFinalNewline = content.endsWith("\n");
|
|
29
|
+
const lines = content.replace(/\n$/, "").split("\n");
|
|
30
|
+
const body = lines.map((l) => `+${l}`).join("\n");
|
|
31
|
+
return header + `--- /dev/null\n+++ b/${rel}\n@@ -0,0 +1,${lines.length} @@\n` + body + (hasFinalNewline ? "\n" : "\n\\n");
|
|
20
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a raw {@link SandboxFileEvent} with lazy git-backed accessors bound to
|
|
35
|
+
* the live handle. `baseSha` is the session baseline (`''` when the workspace
|
|
36
|
+
* isn't a git repo). Never throws — every git/fs failure falls back to `''`
|
|
37
|
+
* (or a synthesized add-patch), but is logged first via `logger` so a failure
|
|
38
|
+
* is observable instead of silently becoming empty data.
|
|
39
|
+
*/
|
|
21
40
|
function buildFileHookEvent(handle, root, baseSha, event, logger) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
if (res.stdout !== "") return res.stdout;
|
|
125
|
-
return synthesizeIfUntracked(rel);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
logger?.warn("sandbox diff() git diff failed", {
|
|
128
|
-
path: event.path,
|
|
129
|
-
error
|
|
130
|
-
});
|
|
131
|
-
return "";
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
return { ...event, before, after, diff };
|
|
41
|
+
const after = async () => {
|
|
42
|
+
if (event.type === "delete") return "";
|
|
43
|
+
try {
|
|
44
|
+
return await handle.fs.read(event.path);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger?.warn("sandbox after() failed to read file", {
|
|
47
|
+
path: event.path,
|
|
48
|
+
error
|
|
49
|
+
});
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const before = async () => {
|
|
54
|
+
if (baseSha === "") return "";
|
|
55
|
+
const rel = relTo(root, event.path);
|
|
56
|
+
try {
|
|
57
|
+
const res = await handle.process.exec(`git show ${q(baseSha)}:${q(rel)}`, { cwd: root });
|
|
58
|
+
if (res.exitCode === 0) return res.stdout;
|
|
59
|
+
logger?.sandbox("before() git show non-zero exit", {
|
|
60
|
+
path: event.path,
|
|
61
|
+
exitCode: res.exitCode,
|
|
62
|
+
stderr: res.stderr
|
|
63
|
+
});
|
|
64
|
+
return "";
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger?.warn("sandbox before() git show failed", {
|
|
67
|
+
path: event.path,
|
|
68
|
+
error
|
|
69
|
+
});
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const synthesizeIfUntracked = async (rel) => {
|
|
74
|
+
const content = await after();
|
|
75
|
+
if (content === "") return "";
|
|
76
|
+
try {
|
|
77
|
+
const ignored = await handle.process.exec(`git check-ignore -q -- ${q(rel)}`, { cwd: root });
|
|
78
|
+
if (ignored.exitCode === 0) {
|
|
79
|
+
logger?.sandbox("sandbox diff() withheld for git-ignored file", { path: event.path });
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
if (ignored.exitCode !== 1) logger?.warn("sandbox diff() git check-ignore non-zero exit", {
|
|
83
|
+
path: event.path,
|
|
84
|
+
exitCode: ignored.exitCode,
|
|
85
|
+
stderr: ignored.stderr
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger?.warn("sandbox diff() git check-ignore failed", {
|
|
89
|
+
path: event.path,
|
|
90
|
+
error
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const res = await handle.process.exec(`git show ${q(baseSha)}:${q(rel)}`, { cwd: root });
|
|
95
|
+
if (res.exitCode === 0) return "";
|
|
96
|
+
logger?.sandbox("sandbox diff() tracked-ness probe non-zero exit (treating as untracked)", {
|
|
97
|
+
path: event.path,
|
|
98
|
+
exitCode: res.exitCode,
|
|
99
|
+
stderr: res.stderr
|
|
100
|
+
});
|
|
101
|
+
return synthesizeAddPatch(rel, content);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger?.warn("sandbox diff() tracked-ness probe failed", {
|
|
104
|
+
path: event.path,
|
|
105
|
+
error
|
|
106
|
+
});
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const diff = async () => {
|
|
111
|
+
if (baseSha === "") {
|
|
112
|
+
if (event.type === "delete") return "";
|
|
113
|
+
return synthesizeAddPatch(relTo(root, event.path), await after());
|
|
114
|
+
}
|
|
115
|
+
const rel = relTo(root, event.path);
|
|
116
|
+
try {
|
|
117
|
+
const res = await handle.process.exec(`git diff ${q(baseSha)} -- ${q(rel)}`, { cwd: root });
|
|
118
|
+
if (res.exitCode !== 0) {
|
|
119
|
+
logger?.warn("sandbox diff() git diff non-zero exit", {
|
|
120
|
+
path: event.path,
|
|
121
|
+
exitCode: res.exitCode,
|
|
122
|
+
stderr: res.stderr
|
|
123
|
+
});
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
if (res.stdout !== "") return res.stdout;
|
|
127
|
+
return synthesizeIfUntracked(rel);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger?.warn("sandbox diff() git diff failed", {
|
|
130
|
+
path: event.path,
|
|
131
|
+
error
|
|
132
|
+
});
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
...event,
|
|
138
|
+
before,
|
|
139
|
+
after,
|
|
140
|
+
diff
|
|
141
|
+
};
|
|
135
142
|
}
|
|
143
|
+
/** Normalize the `fileEvents` option (`boolean | { diff?: boolean }`). */
|
|
136
144
|
function resolveFileEvents(opt) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
if (opt === false) return {
|
|
146
|
+
enabled: false,
|
|
147
|
+
diff: false
|
|
148
|
+
};
|
|
149
|
+
if (opt === void 0 || opt === true) return {
|
|
150
|
+
enabled: true,
|
|
151
|
+
diff: false
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
enabled: true,
|
|
155
|
+
diff: opt.diff === true
|
|
156
|
+
};
|
|
140
157
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//# sourceMappingURL=file-diff.js.map
|
|
158
|
+
//#endregion
|
|
159
|
+
export { buildFileHookEvent, resolveFileEvents };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=file-diff.js.map
|