@agentshouse/kit 0.1.0-alpha.22 → 0.1.0-alpha.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/adapter-mcp.js +57 -2
- package/src/bytes.js +16 -7
- package/src/contract.js +6 -0
- package/src/house.js +4 -0
- package/src/launcher.js +10 -2
- package/src/worker.js +58 -12
- package/src/working-copy.js +18 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
|
|
|
10
10
|
versioned bootstrap once:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
13
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.24/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -781,7 +781,18 @@ Kit reads what the Session was started for from House's
|
|
|
781
781
|
its input, references, requester and the pinned revision's instruction, or the
|
|
782
782
|
sealed conversation turn. It sends that as the first prompt block, followed by
|
|
783
783
|
current House guidance fetched through the Run connection, so the Agent calls
|
|
784
|
-
nothing to learn it. An unreadable request or guidance refuses the turn.
|
|
784
|
+
nothing to learn it. An unreadable request or guidance refuses the turn.
|
|
785
|
+
A session opened with `session/new` gets one more text block ahead of those,
|
|
786
|
+
the same for every adapter kind: the line
|
|
787
|
+
`The agents.house house CLI is available: run house --help.`, the base
|
|
788
|
+
instructions of the Execution attempt plan House acquired, and the current text
|
|
789
|
+
of the owner's How-we-work document `/private/library/how-we-work.md`, which the
|
|
790
|
+
launcher reads with the `shell` tool over the Session's own bridge, following
|
|
791
|
+
House's cursor to the whole text. An absent document, one the Session's ceiling
|
|
792
|
+
cannot read, and empty base instructions add nothing. Any other failed read
|
|
793
|
+
refuses the attempt as `kit_how_we_work_unreadable` with the bridge's cause
|
|
794
|
+
before a prompt is sent. A session reloaded with `session/load` already holds
|
|
795
|
+
the block and gets none. It forwards only adapter-classified User-visible assistant
|
|
785
796
|
text back over the local channel, and leaves reasoning, tool traces, and
|
|
786
797
|
terminal output where they are. The supervised process's own House calls travel
|
|
787
798
|
that same channel through the `house` CLI. The Kit holds the WorkerRunToken, replaces it in place when
|
|
@@ -1312,6 +1323,19 @@ control lease when a resident runs, and which otherwise acquires and releases
|
|
|
1312
1323
|
its own. A Session refuses `--owner`, and a missing bridge is never replaced by
|
|
1313
1324
|
owner authority.
|
|
1314
1325
|
|
|
1326
|
+
A Session's edit too large for one MCP request of 4 MiB goes through House's
|
|
1327
|
+
`prepare_edit_upload` Command on that Session's bridge, naming every changed
|
|
1328
|
+
path, the byte count and the SHA-256 of the bytes to upload under the push's
|
|
1329
|
+
operation. Those bytes are the serialized changes, padded with trailing spaces
|
|
1330
|
+
to one byte over 4 MiB when only the request around them passed the bound,
|
|
1331
|
+
because House takes no smaller upload. Kit sends them to the upload address
|
|
1332
|
+
House returns and then calls `edit` with the paths and the returned upload
|
|
1333
|
+
instead of the changes, so House applies them as the same one edit. The upload
|
|
1334
|
+
is recorded in Kit home before the edit is sent, so a push whose answer was lost
|
|
1335
|
+
resends that same edit; when House no longer holds the uploaded bytes and has
|
|
1336
|
+
not applied the edit, the push uploads them again once under the same operation.
|
|
1337
|
+
A change set beyond House's batch bounds is refused whole and never split.
|
|
1338
|
+
|
|
1315
1339
|
Inside an Agent Session `git` is Kit's shim in front of the installed Git, and
|
|
1316
1340
|
every command runs exactly as that Git runs it. In a copy or one of its linked
|
|
1317
1341
|
worktrees the shim compares that worktree's index before and after each
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.24",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.24/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.24/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/adapter-mcp.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
|
|
3
|
+
HOW_WE_WORK_PATH,
|
|
4
|
+
MCP_PROTOCOL_VERSION,
|
|
5
|
+
SHELL_RENDER,
|
|
6
|
+
} from "./contract.js";
|
|
7
|
+
import { answerText, bridgeCall } from "./mcp.js";
|
|
3
8
|
import { KitRefusal } from "./refusal.js";
|
|
4
9
|
|
|
5
10
|
function readableDiscovery(discovery) {
|
|
@@ -29,3 +34,53 @@ export async function commandHouseInstructions(bridge) {
|
|
|
29
34
|
}
|
|
30
35
|
return frame.result.instructions;
|
|
31
36
|
}
|
|
37
|
+
|
|
38
|
+
function shellAnswer(text) {
|
|
39
|
+
const lines = text.split("\n");
|
|
40
|
+
if (!lines[0].startsWith(`protocol: render=${SHELL_RENDER} `)) return null;
|
|
41
|
+
const exit = Number(lines[2].slice("exit: ".length));
|
|
42
|
+
let at = lines[5]?.startsWith("authority: ") ? 6 : 5;
|
|
43
|
+
if (exit === 0) return { exit, stderr: [], stdout: lines.slice(at).join("\n"), cursor: null };
|
|
44
|
+
const stderr = [];
|
|
45
|
+
while (lines[at]?.startsWith("stderr: ")) stderr.push(lines[at++].slice("stderr: ".length));
|
|
46
|
+
const stdout = lines.slice(at);
|
|
47
|
+
const cursor = stdout.at(-1)?.startsWith("cursor: ") ? stdout.pop().slice("cursor: ".length) : null;
|
|
48
|
+
return { exit, stderr, stdout: stdout.join("\n"), cursor };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function howWeWorkPage(bridge, cursor) {
|
|
52
|
+
const frame = await bridgeCall(bridge, {
|
|
53
|
+
method: "tools/call",
|
|
54
|
+
params: {
|
|
55
|
+
name: "shell",
|
|
56
|
+
arguments: { command: `cat ${HOW_WE_WORK_PATH}`, ...(cursor === null ? {} : { cursor }) },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (frame.error !== undefined) {
|
|
60
|
+
throw new KitRefusal("kit_how_we_work_unreadable", frame.error.message ?? null);
|
|
61
|
+
}
|
|
62
|
+
const text = answerText(frame.result);
|
|
63
|
+
if (frame.result?.isError === true) {
|
|
64
|
+
if (text.split(":")[0] === "room_not_found") return null;
|
|
65
|
+
throw new KitRefusal("kit_how_we_work_unreadable", text);
|
|
66
|
+
}
|
|
67
|
+
const page = shellAnswer(text);
|
|
68
|
+
if (page === null) throw new KitRefusal("kit_how_we_work_unreadable", text);
|
|
69
|
+
if (page.cursor === null && page.exit !== 0) {
|
|
70
|
+
if (page.stderr.length === 1 && page.stderr[0].split(" ")[1] === "path_not_found") return null;
|
|
71
|
+
throw new KitRefusal("kit_how_we_work_unreadable", page.stderr.join("\n"));
|
|
72
|
+
}
|
|
73
|
+
return page;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function commandHouseHowWeWork(bridge) {
|
|
77
|
+
const pages = [];
|
|
78
|
+
let cursor = null;
|
|
79
|
+
do {
|
|
80
|
+
const page = await howWeWorkPage(bridge, cursor);
|
|
81
|
+
if (page === null) return "";
|
|
82
|
+
pages.push(page.stdout);
|
|
83
|
+
cursor = page.cursor;
|
|
84
|
+
} while (cursor !== null);
|
|
85
|
+
return pages.join("\n");
|
|
86
|
+
}
|
package/src/bytes.js
CHANGED
|
@@ -31,6 +31,11 @@ async function downloadBody(response, signal) {
|
|
|
31
31
|
return Buffer.concat(chunks, size);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function framedBody(request) {
|
|
35
|
+
if (request.method !== "POST" || !Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
|
|
36
|
+
return Buffer.concat([Buffer.from(`${request.operation}\n`, "utf8"), request.body]);
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export async function transferBytes(request, dependencies = {}) {
|
|
35
40
|
if (request.method !== "GET" && request.method !== "POST") {
|
|
36
41
|
throw new KitRefusal("kit_byte_method_invalid", request.method);
|
|
@@ -41,23 +46,27 @@ export async function transferBytes(request, dependencies = {}) {
|
|
|
41
46
|
if (typeof request.operation !== "string" || request.operation.length === 0) {
|
|
42
47
|
throw new KitRefusal("kit_byte_operation_invalid");
|
|
43
48
|
}
|
|
49
|
+
const framed = request.framed === true;
|
|
50
|
+
const body = framed ? framedBody(request) : request.body;
|
|
44
51
|
if (request.method === "POST") {
|
|
45
|
-
if (!Buffer.isBuffer(
|
|
46
|
-
if (
|
|
52
|
+
if (!Buffer.isBuffer(body)) throw new KitRefusal("kit_byte_body_invalid");
|
|
53
|
+
if (body.length > BYTE_GRANT_POST_BYTES_MAX) {
|
|
47
54
|
throw new KitRefusal("original_bytes_exceeded");
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
const fetcher = dependencies.fetch ?? fetch;
|
|
51
|
-
const headers =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const headers = framed
|
|
59
|
+
? { "content-type": "text/plain; charset=utf-8" }
|
|
60
|
+
: {
|
|
61
|
+
[BYTE_GRANT_OPERATION_HEADER]: request.operation,
|
|
62
|
+
...(request.method === "POST" ? { "content-type": "application/octet-stream" } : {}),
|
|
63
|
+
};
|
|
55
64
|
let response;
|
|
56
65
|
try {
|
|
57
66
|
response = await fetcher(request.url, {
|
|
58
67
|
method: request.method,
|
|
59
68
|
headers,
|
|
60
|
-
body: request.method === "POST" ?
|
|
69
|
+
body: request.method === "POST" ? body : undefined,
|
|
61
70
|
redirect: "manual",
|
|
62
71
|
signal: request.signal,
|
|
63
72
|
});
|
package/src/contract.js
CHANGED
|
@@ -118,6 +118,10 @@ export const BRIDGE_NONCE_ENVIRONMENT = "HOUSE_KIT_BRIDGE_NONCE";
|
|
|
118
118
|
export const BRIDGE_RECONNECT_MS = 200;
|
|
119
119
|
export const BRIDGE_RECONNECT_MAX_MS = 5_000;
|
|
120
120
|
|
|
121
|
+
export const HOUSE_CLI_LINE = "The agents.house house CLI is available: run house --help.";
|
|
122
|
+
export const HOW_WE_WORK_PATH = "/private/library/how-we-work.md";
|
|
123
|
+
export const SHELL_RENDER = "shell/1";
|
|
124
|
+
|
|
121
125
|
export const ACP_PROTOCOL = 1;
|
|
122
126
|
export const ACP_AUTH_REQUIRED = -32000;
|
|
123
127
|
export const ADAPTER_EXIT_GRACE_MS = 2_000;
|
|
@@ -308,6 +312,8 @@ export const WORKING_COPY_PUSH_METHOD = "house/git/push";
|
|
|
308
312
|
export const WORKING_COPY_INDEX_METHOD = "house/git/index";
|
|
309
313
|
export const WORKING_COPY_PUSH_HELP =
|
|
310
314
|
"Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
|
|
315
|
+
export const WORKING_COPY_UPLOAD_COMMAND = "prepare_edit_upload";
|
|
316
|
+
export const MCP_REQUEST_BODY_MAX_BYTES = 4 * 1024 * 1024;
|
|
311
317
|
|
|
312
318
|
export const MIGRATOR_CONTRACT = "kit-migrator/1";
|
|
313
319
|
export const MIGRATOR_CATEGORY = "migrator";
|
package/src/house.js
CHANGED
|
@@ -855,9 +855,13 @@ function acquiredAssignment(value, declaration) {
|
|
|
855
855
|
if (Number.isNaN(Date.parse(expiresAt))) {
|
|
856
856
|
envelope.refuse(`${declaration}.session.bridge_token_expires_at`);
|
|
857
857
|
}
|
|
858
|
+
if (typeof plan.base_instructions !== "string") {
|
|
859
|
+
envelope.refuse(`${declaration}.plan.base_instructions`);
|
|
860
|
+
}
|
|
858
861
|
return {
|
|
859
862
|
run: envelope.text(acquired.run, `${declaration}.run`, 128),
|
|
860
863
|
agentId: envelope.text(plan.agent_id, `${declaration}.plan.agent_id`, 128),
|
|
864
|
+
baseInstructions: plan.base_instructions,
|
|
861
865
|
workingDirectory: envelope.text(
|
|
862
866
|
plan.working_directory,
|
|
863
867
|
`${declaration}.plan.working_directory`,
|
package/src/launcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { commandHouseInstructions } from "./adapter-mcp.js";
|
|
2
|
+
import { commandHouseHowWeWork, commandHouseInstructions } from "./adapter-mcp.js";
|
|
3
3
|
import {
|
|
4
4
|
compiledContract,
|
|
5
5
|
initializeCommandAdapter,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
startCommandAdapter,
|
|
8
8
|
stopCommandAdapter,
|
|
9
9
|
} from "./adapter.js";
|
|
10
|
-
import { PERMISSION_OPTION_KINDS, SESSION_SET_CONFIG_OPTION } from "./contract.js";
|
|
10
|
+
import { HOUSE_CLI_LINE, PERMISSION_OPTION_KINDS, SESSION_SET_CONFIG_OPTION } from "./contract.js";
|
|
11
11
|
import { observedAssistantDrafts } from "./draft.js";
|
|
12
12
|
import { observedPlans } from "./plan.js";
|
|
13
13
|
import { bridgePeer } from "./peer.js";
|
|
@@ -129,6 +129,11 @@ async function configureSession(acp, contract, sessionId, adapter) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
async function sessionOpening(bridge, baseInstructions) {
|
|
133
|
+
const parts = [HOUSE_CLI_LINE, baseInstructions, await commandHouseHowWeWork(bridge)];
|
|
134
|
+
return [{ type: "text", text: parts.filter((part) => part.trim() !== "").join("\n\n") }];
|
|
135
|
+
}
|
|
136
|
+
|
|
132
137
|
function workingCopyContext(copies) {
|
|
133
138
|
if (!Array.isArray(copies) || copies.length === 0) return [];
|
|
134
139
|
const lines = copies.map((copy) => `${copy.path}: ${copy.repository}`);
|
|
@@ -179,6 +184,8 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
179
184
|
const { acp } = await initializeCommandAdapter(adapter, contract, handlers);
|
|
180
185
|
const instructions = await commandHouseInstructions(bridge);
|
|
181
186
|
let providerSessionId = start.provider_session ?? null;
|
|
187
|
+
const opening =
|
|
188
|
+
providerSessionId === null ? await sessionOpening(bridge, start.base_instructions) : [];
|
|
182
189
|
const setup = { cwd: start.working_directory, mcpServers: [] };
|
|
183
190
|
if (providerSessionId === null) {
|
|
184
191
|
providerSessionId = openedSession(await acp.request("session/new", setup));
|
|
@@ -196,6 +203,7 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
196
203
|
const prompted = await acp.request("session/prompt", {
|
|
197
204
|
sessionId: providerSessionId,
|
|
198
205
|
prompt: [
|
|
206
|
+
...opening,
|
|
199
207
|
{ type: "text", text: JSON.stringify(start.request) },
|
|
200
208
|
{ type: "text", text: instructions },
|
|
201
209
|
...workingCopyContext(start.working_copies),
|
package/src/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
12
12
|
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
13
13
|
import { bridgedFrame, openBridge } from "./bridge.js";
|
|
14
|
+
import { transferBytes } from "./bytes.js";
|
|
14
15
|
import { assistantBlocks } from "./blocks.js";
|
|
15
16
|
import { retryConversationFiles } from "./conversation-originals.js";
|
|
16
17
|
import { openAssistantDrafts } from "./draft.js";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
ownerCaller,
|
|
26
27
|
pushWorkingCopy,
|
|
27
28
|
recordIndexChange,
|
|
29
|
+
uploadedPaths,
|
|
28
30
|
workingCopyPaths,
|
|
29
31
|
} from "./working-copy.js";
|
|
30
32
|
import { readConfiguration } from "./configuration.js";
|
|
@@ -32,6 +34,7 @@ import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
|
32
34
|
import {
|
|
33
35
|
BRIDGE_NONCE_ENVIRONMENT,
|
|
34
36
|
BRIDGE_SOCKET_ENVIRONMENT,
|
|
37
|
+
MCP_REQUEST_BODY_MAX_BYTES,
|
|
35
38
|
PROVIDER_CONTINUATION_UNAVAILABLE,
|
|
36
39
|
WORKER_COMMAND_BATCH_MAX,
|
|
37
40
|
WORKER_COMMAND_POLL_MS,
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
WORKER_SUPERVISION_POLL_MS,
|
|
43
46
|
WORKING_COPY_INDEX_METHOD,
|
|
44
47
|
WORKING_COPY_PUSH_METHOD,
|
|
48
|
+
WORKING_COPY_UPLOAD_COMMAND,
|
|
45
49
|
} from "./contract.js";
|
|
46
50
|
import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
|
|
47
51
|
import {
|
|
@@ -299,20 +303,61 @@ function acceptedWith(text) {
|
|
|
299
303
|
return { refused: null, written: [...written.values()] };
|
|
300
304
|
}
|
|
301
305
|
|
|
306
|
+
function toolFrame(name, args, operation) {
|
|
307
|
+
return bridgedFrame({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, operation);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function uploadBody(changes) {
|
|
311
|
+
const body = Buffer.from(JSON.stringify({ changes }), "utf8");
|
|
312
|
+
if (body.length > MCP_REQUEST_BODY_MAX_BYTES) return body;
|
|
313
|
+
return Buffer.concat([body, Buffer.alloc(MCP_REQUEST_BODY_MAX_BYTES + 1 - body.length, " ")]);
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
async function sessionPush(context, bound, authority, frame, signal) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
317
|
+
const call = async (sent, called) =>
|
|
318
|
+
callHouseMcp(context.session, await sessionAuthority(context.session, bound.session, authority, sent), called, sent);
|
|
319
|
+
const refused = (answer) =>
|
|
320
|
+
answer.error !== undefined
|
|
321
|
+
? refusedWith(answer.error.message ?? "kit_house_refused")
|
|
322
|
+
: answer.result?.isError === true
|
|
323
|
+
? refusedWith(answerText(answer.result))
|
|
324
|
+
: null;
|
|
325
|
+
const stage = async (body, sent) => {
|
|
326
|
+
const bytes = uploadBody(body.changes);
|
|
327
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
328
|
+
const prepare = { paths: uploadedPaths(body.changes), bytes: bytes.length, sha256 };
|
|
329
|
+
const prepared = await call(
|
|
330
|
+
sent,
|
|
331
|
+
toolFrame("run_command", { command: WORKING_COPY_UPLOAD_COMMAND, arguments: prepare }, body.operation_id),
|
|
332
|
+
);
|
|
333
|
+
const refusal = refused(prepared);
|
|
334
|
+
if (refusal !== null) return refusal;
|
|
335
|
+
const transfer = answerYaml(answerText(prepared.result))?.transfer;
|
|
336
|
+
const staged = (
|
|
337
|
+
await transferBytes(
|
|
338
|
+
{ url: transfer?.url, method: "POST", operation: transfer?.operation, body: bytes, framed: true, signal: sent },
|
|
339
|
+
context.session.dependencies,
|
|
340
|
+
)
|
|
341
|
+
).transfer;
|
|
342
|
+
if (typeof staged?.url !== "string" || typeof staged.operation !== "string") {
|
|
343
|
+
throw new KitRefusal("kit_house_answer_unreadable", WORKING_COPY_UPLOAD_COMMAND);
|
|
344
|
+
}
|
|
345
|
+
return { upload: { url: staged.url, operation: staged.operation, bytes: bytes.length, sha256 } };
|
|
346
|
+
};
|
|
347
|
+
const edit = async (body, sent, retain) => {
|
|
348
|
+
let { upload } = body;
|
|
349
|
+
const inline = toolFrame("edit", { changes: body.changes }, body.operation_id);
|
|
350
|
+
if (upload === undefined && Buffer.byteLength(JSON.stringify(inline)) > MCP_REQUEST_BODY_MAX_BYTES) {
|
|
351
|
+
const staged = await stage(body, sent);
|
|
352
|
+
if (staged.upload === undefined) return staged;
|
|
353
|
+
({ upload } = staged);
|
|
354
|
+
await retain(upload);
|
|
355
|
+
}
|
|
356
|
+
const answer = await call(
|
|
311
357
|
sent,
|
|
358
|
+
upload === undefined ? inline : toolFrame("edit", { paths: uploadedPaths(body.changes), upload }, body.operation_id),
|
|
312
359
|
);
|
|
313
|
-
|
|
314
|
-
const text = answerText(answer.result);
|
|
315
|
-
return answer.result?.isError === true ? refusedWith(text) : acceptedWith(text);
|
|
360
|
+
return refused(answer) ?? acceptedWith(answerText(answer.result));
|
|
316
361
|
};
|
|
317
362
|
let outcome;
|
|
318
363
|
try {
|
|
@@ -530,6 +575,7 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
530
575
|
};
|
|
531
576
|
const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
|
|
532
577
|
const start = {
|
|
578
|
+
base_instructions: assignment.baseInstructions,
|
|
533
579
|
working_directory: assignment.workingDirectory,
|
|
534
580
|
working_copies: await appliedWorkingCopies(context),
|
|
535
581
|
execution_authority: context.configuration.executionAuthority,
|
package/src/working-copy.js
CHANGED
|
@@ -914,6 +914,10 @@ function pushedPaths(changes) {
|
|
|
914
914
|
return new Set(changes.flatMap((change) => (change.op === "rename" ? [change.path, change.to] : [change.path])));
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
export function uploadedPaths(changes) {
|
|
918
|
+
return [...pushedPaths(changes)].sort();
|
|
919
|
+
}
|
|
920
|
+
|
|
917
921
|
async function enumerated(context, copy, since, signal) {
|
|
918
922
|
const answer = await door(
|
|
919
923
|
context.session,
|
|
@@ -1104,12 +1108,18 @@ async function expired(context, copy, text, signal) {
|
|
|
1104
1108
|
return finish(context, accepted, signal);
|
|
1105
1109
|
}
|
|
1106
1110
|
|
|
1107
|
-
async function settle(context, copy, caller, signal) {
|
|
1111
|
+
async function settle(context, copy, caller, signal, restaged = false) {
|
|
1108
1112
|
const { push } = copy;
|
|
1109
1113
|
if (!sameCaller(push.caller, caller.binding)) return orphaned(context, copy, signal);
|
|
1114
|
+
let staged = push.upload !== undefined;
|
|
1115
|
+
const retain = async (upload) => {
|
|
1116
|
+
staged = true;
|
|
1117
|
+
await writeCopy(context.directory, { ...copy, push: { ...push, upload } }, signal);
|
|
1118
|
+
};
|
|
1110
1119
|
let answer;
|
|
1111
1120
|
try {
|
|
1112
|
-
|
|
1121
|
+
const { operation_id, changes, upload } = push;
|
|
1122
|
+
answer = await caller.submit({ operation_id, changes, ...(upload === undefined ? {} : { upload }) }, signal, retain);
|
|
1113
1123
|
} catch (error) {
|
|
1114
1124
|
if (!definite(error)) {
|
|
1115
1125
|
throw new KitRefusal("kit_working_copy_push_uncertain", "run house git push again to recover its outcome");
|
|
@@ -1118,6 +1128,12 @@ async function settle(context, copy, caller, signal) {
|
|
|
1118
1128
|
answer = { refused: { code: error.code, text: formatCommandError(error), current: Array.isArray(current) ? current : null } };
|
|
1119
1129
|
}
|
|
1120
1130
|
const { refused } = answer;
|
|
1131
|
+
if (refused?.code === "upload_unavailable" && staged && !restaged) {
|
|
1132
|
+
const { upload, ...unstaged } = push;
|
|
1133
|
+
const cleared = { ...copy, push: unstaged };
|
|
1134
|
+
await writeCopy(context.directory, cleared, signal);
|
|
1135
|
+
return settle(context, cleared, caller, signal, true);
|
|
1136
|
+
}
|
|
1121
1137
|
if (refused?.code === "operation_expired") return expired(context, copy, refused.text, signal);
|
|
1122
1138
|
if (refused === null) {
|
|
1123
1139
|
const pushed = pushedPaths(push.changes);
|