@elie-laloum/outpost 1.1.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.fr.md +251 -0
- package/README.md +226 -0
- package/ROADMAP.md +34 -0
- package/SECURITY.md +15 -0
- package/dist/application/campaign.d.ts +33 -0
- package/dist/application/campaign.js +195 -0
- package/dist/application/campaign.js.map +1 -0
- package/dist/application/execution.d.ts +48 -0
- package/dist/application/execution.js +322 -0
- package/dist/application/execution.js.map +1 -0
- package/dist/application/interactive-brief.d.ts +3 -0
- package/dist/application/interactive-brief.js +35 -0
- package/dist/application/interactive-brief.js.map +1 -0
- package/dist/application/outpost.d.ts +91 -0
- package/dist/application/outpost.js +629 -0
- package/dist/application/outpost.js.map +1 -0
- package/dist/application/remote-workspace.d.ts +11 -0
- package/dist/application/remote-workspace.js +264 -0
- package/dist/application/remote-workspace.js.map +1 -0
- package/dist/application/tasks.d.ts +17 -0
- package/dist/application/tasks.js +36 -0
- package/dist/application/tasks.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +94 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/scaffold.d.ts +28 -0
- package/dist/cli/scaffold.js +169 -0
- package/dist/cli/scaffold.js.map +1 -0
- package/dist/cli/starters.d.ts +3 -0
- package/dist/cli/starters.js +34 -0
- package/dist/cli/starters.js.map +1 -0
- package/dist/domain/backlog.d.ts +16 -0
- package/dist/domain/backlog.js +23 -0
- package/dist/domain/backlog.js.map +1 -0
- package/dist/domain/errors.d.ts +11 -0
- package/dist/domain/errors.js +37 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/ports.d.ts +183 -0
- package/dist/domain/ports.js +2 -0
- package/dist/domain/ports.js.map +1 -0
- package/dist/domain/prompts.d.ts +22 -0
- package/dist/domain/prompts.js +45 -0
- package/dist/domain/prompts.js.map +1 -0
- package/dist/domain/response.d.ts +37 -0
- package/dist/domain/response.js +50 -0
- package/dist/domain/response.js.map +1 -0
- package/dist/domain/workflow.d.ts +68 -0
- package/dist/domain/workflow.js +246 -0
- package/dist/domain/workflow.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/abort.d.ts +1 -0
- package/dist/infrastructure/abort.js +16 -0
- package/dist/infrastructure/abort.js.map +1 -0
- package/dist/infrastructure/conversations.d.ts +30 -0
- package/dist/infrastructure/conversations.js +222 -0
- package/dist/infrastructure/conversations.js.map +1 -0
- package/dist/infrastructure/files.d.ts +5 -0
- package/dist/infrastructure/files.js +85 -0
- package/dist/infrastructure/files.js.map +1 -0
- package/dist/infrastructure/git.d.ts +14 -0
- package/dist/infrastructure/git.js +304 -0
- package/dist/infrastructure/git.js.map +1 -0
- package/dist/infrastructure/journal.d.ts +10 -0
- package/dist/infrastructure/journal.js +53 -0
- package/dist/infrastructure/journal.js.map +1 -0
- package/dist/infrastructure/process.d.ts +6 -0
- package/dist/infrastructure/process.js +133 -0
- package/dist/infrastructure/process.js.map +1 -0
- package/dist/infrastructure/reporter.d.ts +10 -0
- package/dist/infrastructure/reporter.js +47 -0
- package/dist/infrastructure/reporter.js.map +1 -0
- package/dist/infrastructure/settings.d.ts +3 -0
- package/dist/infrastructure/settings.js +41 -0
- package/dist/infrastructure/settings.js.map +1 -0
- package/dist/infrastructure/shutdown.d.ts +1 -0
- package/dist/infrastructure/shutdown.js +67 -0
- package/dist/infrastructure/shutdown.js.map +1 -0
- package/dist/infrastructure/terminal.d.ts +11 -0
- package/dist/infrastructure/terminal.js +19 -0
- package/dist/infrastructure/terminal.js.map +1 -0
- package/dist/infrastructure/transfer.d.ts +3 -0
- package/dist/infrastructure/transfer.js +29 -0
- package/dist/infrastructure/transfer.js.map +1 -0
- package/dist/providers/agents.d.ts +17 -0
- package/dist/providers/agents.js +204 -0
- package/dist/providers/agents.js.map +1 -0
- package/dist/providers/backlogs.d.ts +9 -0
- package/dist/providers/backlogs.js +82 -0
- package/dist/providers/backlogs.js.map +1 -0
- package/dist/providers/cloud-files.d.ts +4 -0
- package/dist/providers/cloud-files.js +66 -0
- package/dist/providers/cloud-files.js.map +1 -0
- package/dist/providers/container.d.ts +21 -0
- package/dist/providers/container.js +310 -0
- package/dist/providers/container.js.map +1 -0
- package/dist/providers/daytona.d.ts +10 -0
- package/dist/providers/daytona.js +153 -0
- package/dist/providers/daytona.js.map +1 -0
- package/dist/providers/docker.d.ts +3 -0
- package/dist/providers/docker.js +3 -0
- package/dist/providers/docker.js.map +1 -0
- package/dist/providers/factories.d.ts +9 -0
- package/dist/providers/factories.js +12 -0
- package/dist/providers/factories.js.map +1 -0
- package/dist/providers/local.d.ts +4 -0
- package/dist/providers/local.js +64 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/podman.d.ts +3 -0
- package/dist/providers/podman.js +3 -0
- package/dist/providers/podman.js.map +1 -0
- package/dist/providers/vercel.d.ts +9 -0
- package/dist/providers/vercel.js +158 -0
- package/dist/providers/vercel.js.map +1 -0
- package/dist/providers/versions.d.ts +4 -0
- package/dist/providers/versions.js +5 -0
- package/dist/providers/versions.js.map +1 -0
- package/docs/api.md +147 -0
- package/docs/architecture.md +21 -0
- package/docs/migration-1.1.fr.md +44 -0
- package/docs/migration-1.1.md +38 -0
- package/docs/operations.md +101 -0
- package/docs/providers.md +119 -0
- package/docs/workflows.md +101 -0
- package/package.json +95 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { join, posix } from "node:path";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { OutpostError, invariant, recordRecovery } from "../domain/errors.js";
|
|
4
|
+
import { validateBrief } from "../domain/prompts.js";
|
|
5
|
+
import { acquireWorkspace, commits, git, } from "../infrastructure/git.js";
|
|
6
|
+
import { nativeConversations } from "../infrastructure/conversations.js";
|
|
7
|
+
import { resolveVariables } from "../infrastructure/settings.js";
|
|
8
|
+
import { executeProcess, quote, requireSuccess, } from "../infrastructure/process.js";
|
|
9
|
+
import { journal } from "../infrastructure/journal.js";
|
|
10
|
+
import { registerCleanup } from "../infrastructure/shutdown.js";
|
|
11
|
+
import { restoreTerminal } from "../infrastructure/terminal.js";
|
|
12
|
+
import { boundedTransfers } from "../infrastructure/transfer.js";
|
|
13
|
+
import { docker } from "../providers/docker.js";
|
|
14
|
+
import { agentVersions } from "../providers/versions.js";
|
|
15
|
+
import { execute, notify, renderBrief, validateDispatch, preflightDispatch, } from "./execution.js";
|
|
16
|
+
import { seedRemote } from "./remote-workspace.js";
|
|
17
|
+
import { completeBrief } from "./interactive-brief.js";
|
|
18
|
+
const workspaces = new WeakMap();
|
|
19
|
+
const storageFor = (agent) => agent.storage ??
|
|
20
|
+
(agent.conversations ? nativeConversations(agent.conversations) : undefined);
|
|
21
|
+
async function startupFailure(workspace, cause, options) {
|
|
22
|
+
try {
|
|
23
|
+
const log = await journal(workspace.repository, options.logging, options.label);
|
|
24
|
+
log.record({
|
|
25
|
+
kind: "phase",
|
|
26
|
+
name: "preparation failed",
|
|
27
|
+
branch: workspace.branch,
|
|
28
|
+
directory: workspace.directory,
|
|
29
|
+
});
|
|
30
|
+
log.record({
|
|
31
|
+
kind: "failure",
|
|
32
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
33
|
+
});
|
|
34
|
+
await log.close();
|
|
35
|
+
recordRecovery(cause, {
|
|
36
|
+
branch: workspace.branch,
|
|
37
|
+
directory: workspace.directory,
|
|
38
|
+
log: log.file,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* Logging must preserve the preparation error. */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function openWorkspace(options = {}) {
|
|
46
|
+
options.signal?.throwIfAborted();
|
|
47
|
+
const lease = await acquireWorkspace(options);
|
|
48
|
+
try {
|
|
49
|
+
await hooks(options.hooks?.workspaceReady ?? [], lease.directory, executeProcess, options.signal);
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
await startupFailure(lease, cause, options);
|
|
53
|
+
await lease.dispose();
|
|
54
|
+
throw cause;
|
|
55
|
+
}
|
|
56
|
+
const state = {
|
|
57
|
+
lease,
|
|
58
|
+
active: false,
|
|
59
|
+
closed: false,
|
|
60
|
+
...(options.hooks ? { hooks: options.hooks } : {}),
|
|
61
|
+
};
|
|
62
|
+
const result = {
|
|
63
|
+
...lease,
|
|
64
|
+
dispatch(options) {
|
|
65
|
+
return dispatch({ ...options, workspace: result });
|
|
66
|
+
},
|
|
67
|
+
sandbox(options = {}) {
|
|
68
|
+
return createSandbox({ ...options, workspace: result });
|
|
69
|
+
},
|
|
70
|
+
attach(options) {
|
|
71
|
+
return attach({ ...options, workspace: result });
|
|
72
|
+
},
|
|
73
|
+
async close(settings = {}) {
|
|
74
|
+
invariant(!state.active, "Close the sandbox before closing its workspace");
|
|
75
|
+
state.closed = true;
|
|
76
|
+
return lease.dispose(settings.preserve);
|
|
77
|
+
},
|
|
78
|
+
async [Symbol.asyncDispose]() {
|
|
79
|
+
await result.close();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
workspaces.set(result, state);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function continuationConfiguration(configuration, next) {
|
|
86
|
+
if (next.repository || next.branch || next.copies) {
|
|
87
|
+
const { workspace, ...rest } = configuration;
|
|
88
|
+
return {
|
|
89
|
+
...(workspace ? { repository: workspace.repository } : {}),
|
|
90
|
+
...rest,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return configuration;
|
|
94
|
+
}
|
|
95
|
+
function warmContinuation(options) {
|
|
96
|
+
invariant(![
|
|
97
|
+
"repository",
|
|
98
|
+
"branch",
|
|
99
|
+
"provider",
|
|
100
|
+
"workspace",
|
|
101
|
+
"copies",
|
|
102
|
+
"hooks",
|
|
103
|
+
"limits",
|
|
104
|
+
"bootstrap",
|
|
105
|
+
"conversationHome",
|
|
106
|
+
"includeUncommitted",
|
|
107
|
+
].some((key) => key in options), "Warm continuation uses its existing sandbox; use dispatch() to change sandbox settings");
|
|
108
|
+
}
|
|
109
|
+
async function hooks(commands, directory, invoke, signal, parallel = false) {
|
|
110
|
+
if (parallel) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const combined = signal
|
|
113
|
+
? AbortSignal.any([signal, controller.signal])
|
|
114
|
+
: controller.signal;
|
|
115
|
+
const outcomes = await Promise.allSettled(commands.map((command) => hooks([command], directory, invoke, combined).catch((cause) => {
|
|
116
|
+
controller.abort(cause);
|
|
117
|
+
throw cause;
|
|
118
|
+
})));
|
|
119
|
+
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
120
|
+
if (failure?.status === "rejected")
|
|
121
|
+
throw failure.reason;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const command of commands)
|
|
125
|
+
await requireSuccess({
|
|
126
|
+
...command,
|
|
127
|
+
directory: command.directory ?? directory,
|
|
128
|
+
deadlineMs: command.deadlineMs ?? 600_000,
|
|
129
|
+
...(signal ? { signal } : {}),
|
|
130
|
+
}, invoke);
|
|
131
|
+
}
|
|
132
|
+
async function prepareAdapter(agent, runtime, signal) {
|
|
133
|
+
if (!agent.conversations)
|
|
134
|
+
return agent;
|
|
135
|
+
const executable = agent.conversations;
|
|
136
|
+
const cli = executable === "claude"
|
|
137
|
+
? `@anthropic-ai/claude-code@${agentVersions.claude}`
|
|
138
|
+
: `@openai/codex@${agentVersions.codex}`;
|
|
139
|
+
const prefix = posix.join(runtime.home, ".outpost-tools"), target = posix.join(prefix, "bin", executable);
|
|
140
|
+
const installed = await requireSuccess({
|
|
141
|
+
executable: "sh",
|
|
142
|
+
arguments: [
|
|
143
|
+
"-c",
|
|
144
|
+
`if command -v ${executable} >/dev/null 2>&1; then command -v ${executable}; elif test -x ${quote(target)}; then printf '%s\\n' ${quote(target)}; else npm install --global --allow-scripts=@anthropic-ai/claude-code --prefix ${quote(prefix)} ${cli} >&2 && printf '%s\\n' ${quote(target)}; fi`,
|
|
145
|
+
],
|
|
146
|
+
signal,
|
|
147
|
+
}, runtime.invoke.bind(runtime));
|
|
148
|
+
const binary = installed.stdout.trim();
|
|
149
|
+
invariant(binary.startsWith("/") && !binary.includes("\n"), "Agent bootstrap returned an invalid executable path");
|
|
150
|
+
return {
|
|
151
|
+
...agent,
|
|
152
|
+
request(input) {
|
|
153
|
+
return { ...agent.request(input), executable: binary };
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function createSandbox(options = {}) {
|
|
158
|
+
options.signal?.throwIfAborted();
|
|
159
|
+
const provider = options.provider ?? docker();
|
|
160
|
+
if (provider.placement === "remote" && !options.workspace && !options.branch)
|
|
161
|
+
options = { ...options, branch: { mode: "integrate" } };
|
|
162
|
+
invariant(!options.workspace ||
|
|
163
|
+
(!options.repository && !options.branch && !options.copies), "A supplied workspace owns its repository, branch and copied inputs");
|
|
164
|
+
const owned = !options.workspace;
|
|
165
|
+
invariant(provider.placement !== "remote" ||
|
|
166
|
+
((options.workspace?.policy ?? options.branch)?.mode !== "current" &&
|
|
167
|
+
(options.workspace || options.branch)), "Remote providers require a named or integration workspace");
|
|
168
|
+
const workspace = options.workspace ?? (await openWorkspace(options));
|
|
169
|
+
const state = workspaces.get(workspace);
|
|
170
|
+
invariant(state && !state.closed && !state.active, "Workspace must be open and cannot belong to another sandbox");
|
|
171
|
+
state.active = true;
|
|
172
|
+
const lifecycle = options.hooks ?? state.hooks;
|
|
173
|
+
const stop = new AbortController();
|
|
174
|
+
const setupSignal = options.signal
|
|
175
|
+
? AbortSignal.any([options.signal, stop.signal])
|
|
176
|
+
: stop.signal;
|
|
177
|
+
let lease, sync;
|
|
178
|
+
const prepared = new Map();
|
|
179
|
+
const staging = join(workspace.repository, ".outpost", "recovery", "conversations");
|
|
180
|
+
try {
|
|
181
|
+
const configured = await resolveVariables(workspace.repository, {}, provider.variables);
|
|
182
|
+
const name = (await git(workspace.repository, ["config", "--get", "user.name"]).catch(() => "Outpost")).trim();
|
|
183
|
+
const email = (await git(workspace.repository, ["config", "--get", "user.email"]).catch(() => "outpost@localhost")).trim();
|
|
184
|
+
const variables = {
|
|
185
|
+
GIT_AUTHOR_NAME: name,
|
|
186
|
+
GIT_COMMITTER_NAME: name,
|
|
187
|
+
GIT_AUTHOR_EMAIL: email,
|
|
188
|
+
GIT_COMMITTER_EMAIL: email,
|
|
189
|
+
...configured,
|
|
190
|
+
};
|
|
191
|
+
lease = boundedTransfers(await provider.acquire({
|
|
192
|
+
repository: workspace.repository,
|
|
193
|
+
directory: workspace.directory,
|
|
194
|
+
gitDirectories: workspace.gitDirectories,
|
|
195
|
+
variables,
|
|
196
|
+
signal: setupSignal,
|
|
197
|
+
}), {
|
|
198
|
+
...(options.limits?.copyMs
|
|
199
|
+
? { deadlineMs: options.limits.copyMs }
|
|
200
|
+
: {}),
|
|
201
|
+
});
|
|
202
|
+
if (provider.placement === "remote") {
|
|
203
|
+
sync = await seedRemote(workspace, lease, {
|
|
204
|
+
...(options.includeUncommitted ? { includeUncommitted: true } : {}),
|
|
205
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
206
|
+
signal: setupSignal,
|
|
207
|
+
});
|
|
208
|
+
for (const path of options.copies ?? [])
|
|
209
|
+
if (await stat(join(workspace.directory, path)).catch((error) => {
|
|
210
|
+
if (error.code === "ENOENT")
|
|
211
|
+
return undefined;
|
|
212
|
+
throw error;
|
|
213
|
+
}))
|
|
214
|
+
await lease.upload(join(workspace.directory, path), posix.join(lease.root, path.replaceAll("\\", "/")));
|
|
215
|
+
if (options.bootstrap !== false && options.agent)
|
|
216
|
+
prepared.set(options.agent, await prepareAdapter(options.agent, lease, setupSignal));
|
|
217
|
+
}
|
|
218
|
+
const initialized = await Promise.allSettled([
|
|
219
|
+
hooks(lifecycle?.hostReady ?? [], workspace.directory, executeProcess, setupSignal),
|
|
220
|
+
hooks(lifecycle?.sandboxReady ?? [], lease.root, lease.invoke.bind(lease), setupSignal, true),
|
|
221
|
+
].map((pending) => pending.catch((cause) => {
|
|
222
|
+
stop.abort(cause);
|
|
223
|
+
throw cause;
|
|
224
|
+
})));
|
|
225
|
+
const failure = initialized.find((item) => item.status === "rejected");
|
|
226
|
+
if (failure?.status === "rejected")
|
|
227
|
+
throw failure.reason;
|
|
228
|
+
}
|
|
229
|
+
catch (cause) {
|
|
230
|
+
await startupFailure(workspace, cause, options);
|
|
231
|
+
await lease?.release().catch(() => undefined);
|
|
232
|
+
state.active = false;
|
|
233
|
+
if (owned)
|
|
234
|
+
await workspace.close();
|
|
235
|
+
throw cause;
|
|
236
|
+
}
|
|
237
|
+
const runtime = lease;
|
|
238
|
+
let busy, closing, closed = false;
|
|
239
|
+
const known = new Set();
|
|
240
|
+
const exclusive = (action) => {
|
|
241
|
+
invariant(!closed && !closing, "Sandbox is closed");
|
|
242
|
+
invariant(!busy, "Sandbox already has an active operation; use another sandbox for parallel work");
|
|
243
|
+
const pending = Promise.resolve().then(action);
|
|
244
|
+
busy = pending;
|
|
245
|
+
void pending.then(() => {
|
|
246
|
+
busy = undefined;
|
|
247
|
+
}, () => {
|
|
248
|
+
busy = undefined;
|
|
249
|
+
});
|
|
250
|
+
return pending;
|
|
251
|
+
};
|
|
252
|
+
const selectAgent = async (selected, signal) => {
|
|
253
|
+
invariant(selected, "Provide an agent on the sandbox or this operation");
|
|
254
|
+
if (!prepared.has(selected))
|
|
255
|
+
prepared.set(selected, provider.placement === "remote" && options.bootstrap !== false
|
|
256
|
+
? await prepareAdapter(selected, runtime, signal)
|
|
257
|
+
: selected);
|
|
258
|
+
const variables = await resolveVariables(workspace.repository, selected.variables, provider.variables);
|
|
259
|
+
const adapter = prepared.get(selected);
|
|
260
|
+
return {
|
|
261
|
+
selected,
|
|
262
|
+
adapter,
|
|
263
|
+
executionLease: {
|
|
264
|
+
...runtime,
|
|
265
|
+
invoke(command) {
|
|
266
|
+
return runtime.invoke({
|
|
267
|
+
...command,
|
|
268
|
+
variables: { ...variables, ...command.variables },
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
const conversationKey = (agent, id) => `${agent.storage?.name ?? agent.conversations ?? agent.name}:${id}`;
|
|
275
|
+
const restore = async (id, agent) => {
|
|
276
|
+
if (known.has(conversationKey(agent, id)))
|
|
277
|
+
return;
|
|
278
|
+
const storage = storageFor(agent);
|
|
279
|
+
invariant(storage, "This adapter does not support native conversations");
|
|
280
|
+
const found = await storage.locate(id, workspace.repository, options.conversationHome);
|
|
281
|
+
if (provider.placement !== "host" ||
|
|
282
|
+
workspace.directory !== workspace.repository)
|
|
283
|
+
await storage.restore(found, {
|
|
284
|
+
repository: workspace.repository,
|
|
285
|
+
sandbox: runtime,
|
|
286
|
+
staging,
|
|
287
|
+
});
|
|
288
|
+
known.add(conversationKey(agent, id));
|
|
289
|
+
};
|
|
290
|
+
const result = {
|
|
291
|
+
workspace,
|
|
292
|
+
root: runtime.root,
|
|
293
|
+
dispatch(dispatch) {
|
|
294
|
+
validateDispatch(dispatch);
|
|
295
|
+
return exclusive(async () => {
|
|
296
|
+
await preflightDispatch(dispatch, workspace.repository, dispatch.agent ?? options.agent);
|
|
297
|
+
const signal = dispatch.signal
|
|
298
|
+
? AbortSignal.any([dispatch.signal, stop.signal])
|
|
299
|
+
: stop.signal;
|
|
300
|
+
const { selected, adapter, executionLease } = await selectAgent(dispatch.agent ?? options.agent, signal);
|
|
301
|
+
if (dispatch.continuation)
|
|
302
|
+
await restore(dispatch.continuation.id, selected);
|
|
303
|
+
const baseline = (await git(workspace.directory, ["rev-parse", "HEAD"])).trim();
|
|
304
|
+
const log = await journal(workspace.repository, dispatch.logging ?? options.logging, dispatch.label);
|
|
305
|
+
let execution, transcript;
|
|
306
|
+
const captured = new Map();
|
|
307
|
+
const storage = storageFor(selected);
|
|
308
|
+
let failure;
|
|
309
|
+
let conversation = dispatch.continuation?.id;
|
|
310
|
+
const conversations = new Set(conversation ? [conversation] : []);
|
|
311
|
+
const save = async (id) => {
|
|
312
|
+
invariant(storage, "Conversation storage is unavailable");
|
|
313
|
+
const location = await storage.capture(id, {
|
|
314
|
+
repository: workspace.repository,
|
|
315
|
+
sandbox: runtime,
|
|
316
|
+
staging,
|
|
317
|
+
...(options.conversationHome
|
|
318
|
+
? { home: options.conversationHome }
|
|
319
|
+
: {}),
|
|
320
|
+
...(dispatch.warn ? { warn: dispatch.warn } : {}),
|
|
321
|
+
local: provider.placement === "host",
|
|
322
|
+
});
|
|
323
|
+
captured.set(id, location);
|
|
324
|
+
transcript = location;
|
|
325
|
+
return location;
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
execution = await execute(workspace, executionLease, adapter, provider.placement === "host", {
|
|
329
|
+
...dispatch,
|
|
330
|
+
signal,
|
|
331
|
+
observe(event) {
|
|
332
|
+
if (event.kind === "conversation") {
|
|
333
|
+
conversation = event.id;
|
|
334
|
+
conversations.add(event.id);
|
|
335
|
+
known.add(conversationKey(selected, event.id));
|
|
336
|
+
}
|
|
337
|
+
log.record(event);
|
|
338
|
+
notify(dispatch.observe, event);
|
|
339
|
+
},
|
|
340
|
+
}, async (turn) => {
|
|
341
|
+
if (!turn.conversation || !storage || selected.capture === false)
|
|
342
|
+
return turn;
|
|
343
|
+
const location = await save(turn.conversation);
|
|
344
|
+
const usage = selected.transcriptUsage?.(await readFile(location.file, "utf8"));
|
|
345
|
+
return {
|
|
346
|
+
...turn,
|
|
347
|
+
transcript: location.file,
|
|
348
|
+
...(usage ? { usage } : {}),
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (cause) {
|
|
353
|
+
failure = cause;
|
|
354
|
+
log.record({
|
|
355
|
+
kind: "failure",
|
|
356
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await sync?.pull();
|
|
361
|
+
if (storage && selected.capture !== false)
|
|
362
|
+
for (const id of conversations)
|
|
363
|
+
if (failure || !captured.has(id))
|
|
364
|
+
await save(id);
|
|
365
|
+
}
|
|
366
|
+
catch (cause) {
|
|
367
|
+
failure = failure
|
|
368
|
+
? new AggregateError([failure, cause], "Execution and recovery both failed")
|
|
369
|
+
: cause;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await log.close();
|
|
373
|
+
}
|
|
374
|
+
catch (cause) {
|
|
375
|
+
failure ??= cause;
|
|
376
|
+
}
|
|
377
|
+
const changes = await commits(workspace.directory, baseline, options.limits?.collectMs);
|
|
378
|
+
if (failure) {
|
|
379
|
+
recordRecovery(failure, {
|
|
380
|
+
branch: workspace.branch,
|
|
381
|
+
directory: workspace.directory,
|
|
382
|
+
commits: changes,
|
|
383
|
+
transcript: transcript?.file,
|
|
384
|
+
log: log.file,
|
|
385
|
+
});
|
|
386
|
+
throw failure;
|
|
387
|
+
}
|
|
388
|
+
invariant(execution, "Execution did not produce a result");
|
|
389
|
+
return {
|
|
390
|
+
...execution,
|
|
391
|
+
branch: workspace.branch,
|
|
392
|
+
directory: workspace.directory,
|
|
393
|
+
commits: changes,
|
|
394
|
+
...(transcript ? { transcript: transcript.file } : {}),
|
|
395
|
+
...(log.file ? { log: log.file } : {}),
|
|
396
|
+
resume(next) {
|
|
397
|
+
warmContinuation(next);
|
|
398
|
+
invariant(conversation, "No conversation was emitted");
|
|
399
|
+
return result.resume(conversation, { agent: selected, ...next });
|
|
400
|
+
},
|
|
401
|
+
fork(next) {
|
|
402
|
+
warmContinuation(next);
|
|
403
|
+
invariant(conversation, "No conversation was emitted");
|
|
404
|
+
return result.fork(conversation, { agent: selected, ...next });
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
resume(id, dispatch) {
|
|
410
|
+
return result.dispatch({ ...dispatch, continuation: { id } });
|
|
411
|
+
},
|
|
412
|
+
fork(id, dispatch) {
|
|
413
|
+
return result.dispatch({ ...dispatch, continuation: { id, fork: true } });
|
|
414
|
+
},
|
|
415
|
+
attach(settings = {}) {
|
|
416
|
+
settings.signal?.throwIfAborted();
|
|
417
|
+
validateBrief(settings.brief, true);
|
|
418
|
+
return exclusive(async () => {
|
|
419
|
+
const signal = settings.signal
|
|
420
|
+
? AbortSignal.any([settings.signal, stop.signal])
|
|
421
|
+
: stop.signal;
|
|
422
|
+
const { selected, adapter, executionLease } = await selectAgent(settings.agent ?? options.agent, signal);
|
|
423
|
+
if (settings.continuation)
|
|
424
|
+
await restore(settings.continuation.id, selected);
|
|
425
|
+
const brief = await completeBrief(settings.brief, signal, settings.ask);
|
|
426
|
+
const text = brief
|
|
427
|
+
? await renderBrief(brief, workspace, executionLease, provider.placement === "host", settings)
|
|
428
|
+
: undefined;
|
|
429
|
+
const command = adapter.request({
|
|
430
|
+
interactive: true,
|
|
431
|
+
...(text === undefined ? {} : { text }),
|
|
432
|
+
...(settings.continuation
|
|
433
|
+
? { continuation: settings.continuation }
|
|
434
|
+
: {}),
|
|
435
|
+
});
|
|
436
|
+
const baseline = (await git(workspace.directory, ["rev-parse", "HEAD"])).trim();
|
|
437
|
+
let output;
|
|
438
|
+
try {
|
|
439
|
+
output = await executionLease.invoke({
|
|
440
|
+
...command,
|
|
441
|
+
...(settings.terminal ? { terminal: settings.terminal } : {}),
|
|
442
|
+
deadlineMs: 86_400_000,
|
|
443
|
+
signal: settings.signal
|
|
444
|
+
? AbortSignal.any([settings.signal, stop.signal])
|
|
445
|
+
: stop.signal,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
restoreTerminal();
|
|
450
|
+
await sync?.pull();
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
...output,
|
|
454
|
+
branch: workspace.branch,
|
|
455
|
+
directory: workspace.directory,
|
|
456
|
+
commits: await commits(workspace.directory, baseline, options.limits?.collectMs),
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
},
|
|
460
|
+
command(command) {
|
|
461
|
+
command.signal?.throwIfAborted();
|
|
462
|
+
return exclusive(async () => {
|
|
463
|
+
try {
|
|
464
|
+
return await runtime.invoke({
|
|
465
|
+
...command,
|
|
466
|
+
signal: command.signal
|
|
467
|
+
? AbortSignal.any([command.signal, stop.signal])
|
|
468
|
+
: stop.signal,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
await sync?.pull();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
close(settings = {}) {
|
|
477
|
+
if (closing)
|
|
478
|
+
return closing;
|
|
479
|
+
closing = (async () => {
|
|
480
|
+
stop.abort(new OutpostError("aborted", "Sandbox closed"));
|
|
481
|
+
await busy?.catch(() => undefined);
|
|
482
|
+
let failure;
|
|
483
|
+
try {
|
|
484
|
+
await runtime.release();
|
|
485
|
+
await sync?.close();
|
|
486
|
+
}
|
|
487
|
+
catch (cause) {
|
|
488
|
+
failure = cause;
|
|
489
|
+
}
|
|
490
|
+
state.active = false;
|
|
491
|
+
closed = true;
|
|
492
|
+
unregister();
|
|
493
|
+
const disposal = owned
|
|
494
|
+
? await workspace.close({ preserve: settings.preserve || !!failure })
|
|
495
|
+
: {};
|
|
496
|
+
if (failure)
|
|
497
|
+
throw failure;
|
|
498
|
+
return disposal;
|
|
499
|
+
})();
|
|
500
|
+
return closing;
|
|
501
|
+
},
|
|
502
|
+
async [Symbol.asyncDispose]() {
|
|
503
|
+
await result.close();
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
const unregister = registerCleanup(() => result.close({ preserve: true }));
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
export async function dispatch(options) {
|
|
510
|
+
options.signal?.throwIfAborted();
|
|
511
|
+
await preflightDispatch(options, options.workspace?.repository ?? options.repository ?? process.cwd(), options.agent);
|
|
512
|
+
if ((options.passes ?? 1) > 1) {
|
|
513
|
+
const outputs = [];
|
|
514
|
+
for (let index = 0; index < options.passes; index++) {
|
|
515
|
+
options.signal?.throwIfAborted();
|
|
516
|
+
const output = await dispatch({
|
|
517
|
+
...options,
|
|
518
|
+
passes: 1,
|
|
519
|
+
observe: (event) => notify(options.observe, { ...event, pass: index + 1 }),
|
|
520
|
+
});
|
|
521
|
+
outputs.push(output);
|
|
522
|
+
if (output.completed)
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
const last = outputs.at(-1);
|
|
526
|
+
return {
|
|
527
|
+
...last,
|
|
528
|
+
text: outputs.map((output) => output.text).join("\n"),
|
|
529
|
+
turns: outputs.flatMap((output) => output.turns),
|
|
530
|
+
commits: outputs.flatMap((output) => output.commits),
|
|
531
|
+
usage: outputs.reduce((sum, output) => ({
|
|
532
|
+
input: sum.input + output.usage.input,
|
|
533
|
+
cached: sum.cached + output.usage.cached,
|
|
534
|
+
output: sum.output + output.usage.output,
|
|
535
|
+
...(sum.cacheCreated !== undefined ||
|
|
536
|
+
output.usage.cacheCreated !== undefined
|
|
537
|
+
? {
|
|
538
|
+
cacheCreated: (sum.cacheCreated ?? 0) + (output.usage.cacheCreated ?? 0),
|
|
539
|
+
}
|
|
540
|
+
: {}),
|
|
541
|
+
}), {
|
|
542
|
+
input: 0,
|
|
543
|
+
cached: 0,
|
|
544
|
+
output: 0,
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (options.continuation) {
|
|
549
|
+
const storage = storageFor(options.agent);
|
|
550
|
+
invariant(storage, "This adapter does not support native conversations");
|
|
551
|
+
await storage.locate(options.continuation.id, options.workspace?.repository ?? options.repository ?? process.cwd(), options.conversationHome);
|
|
552
|
+
}
|
|
553
|
+
const sandbox = await createSandbox(options);
|
|
554
|
+
const { brief: _brief, response: _response, continuation: _continuation, passes: _passes, ...configuration } = options;
|
|
555
|
+
let successful = false;
|
|
556
|
+
try {
|
|
557
|
+
const output = await sandbox.dispatch(options);
|
|
558
|
+
await sandbox.workspace.integrate();
|
|
559
|
+
successful = true;
|
|
560
|
+
const disposed = await sandbox.close();
|
|
561
|
+
const continuation = output.conversation;
|
|
562
|
+
return {
|
|
563
|
+
...output,
|
|
564
|
+
...disposed,
|
|
565
|
+
resume(next) {
|
|
566
|
+
invariant(continuation, "No conversation was emitted");
|
|
567
|
+
return dispatch({
|
|
568
|
+
...continuationConfiguration(configuration, next),
|
|
569
|
+
...next,
|
|
570
|
+
continuation: { id: continuation },
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
fork(next) {
|
|
574
|
+
invariant(continuation, "No conversation was emitted");
|
|
575
|
+
return dispatch({
|
|
576
|
+
...continuationConfiguration(configuration, next),
|
|
577
|
+
...next,
|
|
578
|
+
continuation: { id: continuation, fork: true },
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
catch (cause) {
|
|
584
|
+
recordRecovery(cause, {
|
|
585
|
+
branch: sandbox.workspace.branch,
|
|
586
|
+
directory: sandbox.workspace.directory,
|
|
587
|
+
});
|
|
588
|
+
throw cause;
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
if (!successful)
|
|
592
|
+
await sandbox.close({ preserve: true });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
export async function attach(options) {
|
|
596
|
+
options.signal?.throwIfAborted();
|
|
597
|
+
validateBrief(options.brief, true);
|
|
598
|
+
const brief = await completeBrief(options.brief, options.signal, options.ask);
|
|
599
|
+
if (brief)
|
|
600
|
+
options = { ...options, brief };
|
|
601
|
+
if (options.continuation) {
|
|
602
|
+
const storage = storageFor(options.agent);
|
|
603
|
+
invariant(storage, "This adapter does not support native conversations");
|
|
604
|
+
await storage.locate(options.continuation.id, options.workspace?.repository ?? options.repository ?? process.cwd(), options.conversationHome);
|
|
605
|
+
}
|
|
606
|
+
const sandbox = await createSandbox(options);
|
|
607
|
+
let successful = false;
|
|
608
|
+
try {
|
|
609
|
+
const baseline = (await git(sandbox.workspace.directory, ["rev-parse", "HEAD"])).trim();
|
|
610
|
+
const result = await sandbox.attach(options);
|
|
611
|
+
const changes = await commits(sandbox.workspace.directory, baseline, options.limits?.collectMs);
|
|
612
|
+
if (result.status === 0)
|
|
613
|
+
await sandbox.workspace.integrate();
|
|
614
|
+
const disposal = await sandbox.close({ preserve: result.status !== 0 });
|
|
615
|
+
successful = true;
|
|
616
|
+
return {
|
|
617
|
+
...result,
|
|
618
|
+
...disposal,
|
|
619
|
+
branch: sandbox.workspace.branch,
|
|
620
|
+
directory: sandbox.workspace.directory,
|
|
621
|
+
commits: changes,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
if (!successful)
|
|
626
|
+
await sandbox.close({ preserve: true });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
//# sourceMappingURL=outpost.js.map
|