@odla-ai/harness 0.6.0 → 0.7.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/README.md +12 -12
- package/dist/{chunk-ZYNGL5SC.js → chunk-GYWQM76X.js} +197 -39
- package/dist/chunk-GYWQM76X.js.map +1 -0
- package/dist/code-runtime-cli.cjs +197 -39
- package/dist/code-runtime-cli.cjs.map +1 -1
- package/dist/code-runtime-cli.js +4 -4
- package/dist/code-runtime-cli.js.map +1 -1
- package/dist/node.cjs +197 -39
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +19 -13
- package/dist/node.d.ts +19 -13
- package/dist/node.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-ZYNGL5SC.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
@odla-ai/harness is the experimental coding-task boundary shared by odla
|
|
4
4
|
Studio, a trusted Mac/Linux runner, and an untrusted coding-agent container.
|
|
5
|
-
It owns the protocol and safe runner defaults; agent implementations such as
|
|
5
|
+
It owns the protocol and safe runner defaults; agent implementations such as Theseus
|
|
6
6
|
remain replaceable adapters.
|
|
7
7
|
|
|
8
8
|
Harness can stage a bounded, secret-filtered copy of a local checkout. The
|
|
9
|
-
brokered
|
|
9
|
+
brokered Theseus agent receives no workspace mount at all: reads and mutations cross
|
|
10
10
|
the typed tool channel into that disposable snapshot. The original checkout,
|
|
11
11
|
terminal credential, provider credentials, container-engine socket, and
|
|
12
|
-
network are never available to
|
|
12
|
+
network are never available to Theseus. Inference requests cross the same JSONL
|
|
13
13
|
channel and are fulfilled by the control plane through @odla-ai/ai.
|
|
14
14
|
|
|
15
15
|
> **Ask the runbooks first.** odla's operational procedures live in a database,
|
|
@@ -32,7 +32,7 @@ npx @odla-ai/cli code connect \
|
|
|
32
32
|
--env dev
|
|
33
33
|
~~~
|
|
34
34
|
|
|
35
|
-
The terminal verifies the checkout and isolation engine, builds the
|
|
35
|
+
The terminal verifies the checkout and isolation engine, builds the Theseus adapter
|
|
36
36
|
bundled with the exact CLI release into a content-addressed local image, prepares
|
|
37
37
|
each digest-pinned recipe image, and sends the connection request outbound to odla. The signed-in app
|
|
38
38
|
owner approves it; no bearer is copied from the browser. The process stays in
|
|
@@ -49,20 +49,20 @@ otherwise Harness receives the immutable sources from the Registry. References
|
|
|
49
49
|
are materialized under `.odla-references/<alias>` and the broker rejects every
|
|
50
50
|
attempt to patch them. A checkpoint patch is computed against the selected
|
|
51
51
|
primary Git base, so local developer changes remain explicit provenance
|
|
52
|
-
alongside
|
|
52
|
+
alongside Theseus's changes.
|
|
53
53
|
|
|
54
54
|
On Apple Silicon with macOS 26, install Apple's lightweight runtime once with
|
|
55
55
|
`brew install container`; `code connect` selects it and starts its user service.
|
|
56
56
|
On Linux, auto requires rootless Podman. Podman Machine is the macOS fallback;
|
|
57
57
|
Docker is an explicit compatibility adapter only. Recipe images must be
|
|
58
|
-
digest-pinned; the
|
|
59
|
-
and requires no registry credential. All adapters run
|
|
60
|
-
non-root with bounded CPU/memory;
|
|
58
|
+
digest-pinned; the Theseus adapter's local tag contains the SHA-256 of its CLI bundle
|
|
59
|
+
and requires no registry credential. All adapters run Theseus networkless and
|
|
60
|
+
non-root with bounded CPU/memory; Theseus has no
|
|
61
61
|
checkout or engine-socket mount.
|
|
62
62
|
|
|
63
63
|
## Experimental task runner
|
|
64
64
|
|
|
65
|
-
The older generic task queue is separate from the personal
|
|
65
|
+
The older generic task queue is separate from the personal Theseus terminal flow.
|
|
66
66
|
It lives under **Code → Task runners · advanced**. To exercise it, register a
|
|
67
67
|
workspace and keep its runner token outside shell history:
|
|
68
68
|
|
|
@@ -93,7 +93,7 @@ new copy.
|
|
|
93
93
|
## Build recipes
|
|
94
94
|
|
|
95
95
|
The Code terminal advertises a closed recipe catalog. The content-addressed,
|
|
96
|
-
CLI-embedded
|
|
96
|
+
CLI-embedded Theseus adapter and every registry-digest-pinned recipe are prepared
|
|
97
97
|
before approval:
|
|
98
98
|
|
|
99
99
|
~~~json
|
|
@@ -111,7 +111,7 @@ before approval:
|
|
|
111
111
|
|
|
112
112
|
The runtime validates HTTPS, never places its credential in a request body,
|
|
113
113
|
never opens a listener, and rejects malformed or cross-host binding responses.
|
|
114
|
-
|
|
114
|
+
Theseus reads, patches, and runs only registered build recipes through the typed
|
|
115
115
|
CaMeL broker. Checkpoint-stop captures a bounded combined patch; resume verifies
|
|
116
116
|
it against the same selected Git base and exact local source descriptor.
|
|
117
117
|
|
|
@@ -128,7 +128,7 @@ attempt.cancel, and attempt.complete.
|
|
|
128
128
|
|
|
129
129
|
## CaMeL tool and build boundary
|
|
130
130
|
|
|
131
|
-
`createCodeToolBroker` is the trusted implementation for the three
|
|
131
|
+
`createCodeToolBroker` is the trusted implementation for the three Theseus tools:
|
|
132
132
|
registered staged-file reads, confined unified patches, and app-registered
|
|
133
133
|
build recipes. Each request is labelled and evaluated by @odla-ai/camel before
|
|
134
134
|
an effect. Patch data remains Prompt-Unsafe payload; file and recipe selectors
|
|
@@ -882,7 +882,7 @@ var CodeRuntimeCheckpointManager = class {
|
|
|
882
882
|
await this.options.event(command, { type: "message", actor: "system", body: prepared.note }, active.conversationRefs).catch(() => void 0);
|
|
883
883
|
await active.workspace.cleanup();
|
|
884
884
|
await this.options.event(command, { type: "status", status: "checkpointed" }, active.conversationRefs).catch(() => void 0);
|
|
885
|
-
return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "
|
|
885
|
+
return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Theseus stopped at a portable checkpoint" };
|
|
886
886
|
}
|
|
887
887
|
async acknowledged(command, result) {
|
|
888
888
|
if (command.kind !== "checkpoint_stop" || result.status !== "checkpointed") return false;
|
|
@@ -1118,7 +1118,7 @@ async function materializeCommandWorkspace(input) {
|
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
1120
|
// src/code-agent-skill.ts
|
|
1121
|
-
var V1_SYSTEM_PROMPT = `You are
|
|
1121
|
+
var V1_SYSTEM_PROMPT = `You are Theseus, the coding agent inside an odla Code harness.
|
|
1122
1122
|
Use only the odla_read, odla_apply_git_diff, and odla_run_recipe tools.
|
|
1123
1123
|
For mutations, call odla_apply_git_diff with raw git diff text. It must start
|
|
1124
1124
|
with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
|
|
@@ -1128,7 +1128,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
|
|
|
1128
1128
|
var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
|
|
1129
1129
|
Start by orienting: odla_list shows the files in the workspace and odla_search
|
|
1130
1130
|
finds a literal string across them. Prefer those over guessing a path.
|
|
1131
|
-
Then odla_read a bounded range, and odla_apply_git_diff to mutate.
|
|
1131
|
+
Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
|
|
1132
|
+
need several independent searches or file ranges, issue those read-only calls
|
|
1133
|
+
together in one turn; their results stay ordered and the harness overlaps them.
|
|
1134
|
+
Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
|
|
1132
1135
|
For mutations, call odla_apply_git_diff with raw git diff text. It must start
|
|
1133
1136
|
with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
|
|
1134
1137
|
headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
|
|
@@ -1159,18 +1162,26 @@ var SYSTEM_PROMPT_FOR = {
|
|
|
1159
1162
|
};
|
|
1160
1163
|
function codeSkill(opts) {
|
|
1161
1164
|
let seq = 0;
|
|
1165
|
+
let nextCompletion = 1;
|
|
1166
|
+
const completed = /* @__PURE__ */ new Map();
|
|
1162
1167
|
const call = async (tool, input, signal) => {
|
|
1168
|
+
const sequence = ++seq;
|
|
1163
1169
|
const startedAt = Date.now();
|
|
1164
1170
|
const response2 = await opts.broker.execute(
|
|
1165
1171
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
1166
|
-
{ requestId: `bench-${tool}-${
|
|
1172
|
+
{ requestId: `bench-${tool}-${sequence}`, tool, input }
|
|
1167
1173
|
);
|
|
1168
|
-
|
|
1174
|
+
completed.set(sequence, {
|
|
1169
1175
|
tool,
|
|
1170
1176
|
ok: response2.ok,
|
|
1171
1177
|
durationMs: Date.now() - startedAt,
|
|
1172
1178
|
...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
|
|
1173
1179
|
});
|
|
1180
|
+
while (completed.has(nextCompletion)) {
|
|
1181
|
+
const completion = completed.get(nextCompletion);
|
|
1182
|
+
completed.delete(nextCompletion++);
|
|
1183
|
+
opts.onToolCall?.(completion);
|
|
1184
|
+
}
|
|
1174
1185
|
return { content: response2.content, isError: !response2.ok };
|
|
1175
1186
|
};
|
|
1176
1187
|
const read2 = {
|
|
@@ -1186,6 +1197,7 @@ function codeSkill(opts) {
|
|
|
1186
1197
|
},
|
|
1187
1198
|
additionalProperties: false
|
|
1188
1199
|
},
|
|
1200
|
+
concurrency: "parallel",
|
|
1189
1201
|
handler: (input, ctx) => call("sandbox.read", input, ctx.signal)
|
|
1190
1202
|
};
|
|
1191
1203
|
const applyPatch = {
|
|
@@ -1201,11 +1213,17 @@ function codeSkill(opts) {
|
|
|
1201
1213
|
};
|
|
1202
1214
|
const runRecipe = {
|
|
1203
1215
|
name: "odla_run_recipe",
|
|
1204
|
-
description:
|
|
1216
|
+
description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
|
|
1205
1217
|
inputSchema: {
|
|
1206
1218
|
type: "object",
|
|
1207
1219
|
required: ["recipeId"],
|
|
1208
|
-
properties: { recipeId: {
|
|
1220
|
+
properties: { recipeId: {
|
|
1221
|
+
type: "string",
|
|
1222
|
+
minLength: 1,
|
|
1223
|
+
maxLength: 120,
|
|
1224
|
+
pattern: "^[a-zA-Z0-9._:-]+$",
|
|
1225
|
+
...opts.recipeIds?.length ? { enum: [...opts.recipeIds] } : {}
|
|
1226
|
+
} },
|
|
1209
1227
|
additionalProperties: false
|
|
1210
1228
|
},
|
|
1211
1229
|
handler: (input, ctx) => call("sandbox.run_recipe", input, ctx.signal)
|
|
@@ -1221,6 +1239,7 @@ function codeSkill(opts) {
|
|
|
1221
1239
|
},
|
|
1222
1240
|
additionalProperties: false
|
|
1223
1241
|
},
|
|
1242
|
+
concurrency: "parallel",
|
|
1224
1243
|
handler: (input, ctx) => call("sandbox.list", input, ctx.signal)
|
|
1225
1244
|
};
|
|
1226
1245
|
const searchFiles = {
|
|
@@ -1237,11 +1256,13 @@ function codeSkill(opts) {
|
|
|
1237
1256
|
},
|
|
1238
1257
|
additionalProperties: false
|
|
1239
1258
|
},
|
|
1259
|
+
concurrency: "parallel",
|
|
1240
1260
|
handler: (input, ctx) => call("sandbox.search", input, ctx.signal)
|
|
1241
1261
|
};
|
|
1242
1262
|
const graphTool = (name, tool, description, required) => ({
|
|
1243
1263
|
name,
|
|
1244
1264
|
description,
|
|
1265
|
+
concurrency: "parallel",
|
|
1245
1266
|
inputSchema: {
|
|
1246
1267
|
type: "object",
|
|
1247
1268
|
...required ? { required: ["query"] } : {},
|
|
@@ -1293,6 +1314,7 @@ async function runCodeAgent(options) {
|
|
|
1293
1314
|
lease: options.lease,
|
|
1294
1315
|
workspaceDir: options.workspaceDir,
|
|
1295
1316
|
surface,
|
|
1317
|
+
...options.recipeIds ? { recipeIds: options.recipeIds } : {},
|
|
1296
1318
|
onToolCall: (call) => {
|
|
1297
1319
|
toolCalls.push(call);
|
|
1298
1320
|
options.onToolCall?.(call);
|
|
@@ -1324,7 +1346,7 @@ async function runCodeAgent(options) {
|
|
|
1324
1346
|
import { extractText } from "@odla-ai/ai";
|
|
1325
1347
|
async function runCodeAgentAttempt(options) {
|
|
1326
1348
|
try {
|
|
1327
|
-
const surface = options.surface ?? "
|
|
1349
|
+
const surface = options.surface ?? "v3";
|
|
1328
1350
|
const { run } = await runCodeAgent({
|
|
1329
1351
|
inference: options.inference,
|
|
1330
1352
|
broker: options.broker,
|
|
@@ -1335,6 +1357,7 @@ async function runCodeAgentAttempt(options) {
|
|
|
1335
1357
|
// id only labels the request the control plane is about to rewrite.
|
|
1336
1358
|
model: "brokered",
|
|
1337
1359
|
surface,
|
|
1360
|
+
...options.recipeIds ? { recipeIds: options.recipeIds } : {},
|
|
1338
1361
|
...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
|
|
1339
1362
|
...options.budget ? { budget: options.budget } : {},
|
|
1340
1363
|
...options.signal ? { signal: options.signal } : {},
|
|
@@ -1429,11 +1452,30 @@ function createCodeRuntimeInference(options) {
|
|
|
1429
1452
|
}
|
|
1430
1453
|
|
|
1431
1454
|
// src/code-tool-discovery.ts
|
|
1455
|
+
import { spawn as spawn3 } from "child_process";
|
|
1432
1456
|
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
1433
1457
|
import { relative as relative2, resolve as resolve4 } from "path";
|
|
1434
1458
|
var DEFAULT_MAX_FILES = 2e4;
|
|
1435
1459
|
var DEFAULT_MAX_RESULTS = 100;
|
|
1436
1460
|
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
1461
|
+
function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
|
|
1462
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
1463
|
+
return {
|
|
1464
|
+
files(root) {
|
|
1465
|
+
const existing = cache2.get(root);
|
|
1466
|
+
if (existing) return existing;
|
|
1467
|
+
const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
|
|
1468
|
+
cache2.set(root, pending);
|
|
1469
|
+
void pending.catch(() => {
|
|
1470
|
+
if (cache2.get(root) === pending) cache2.delete(root);
|
|
1471
|
+
});
|
|
1472
|
+
return pending;
|
|
1473
|
+
},
|
|
1474
|
+
invalidate(root) {
|
|
1475
|
+
cache2.delete(root);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1437
1479
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
1438
1480
|
const paths = [];
|
|
1439
1481
|
const walk = async (directory) => {
|
|
@@ -1464,28 +1506,122 @@ function listWorkspace(paths, options = {}) {
|
|
|
1464
1506
|
return scoped.slice(0, max);
|
|
1465
1507
|
}
|
|
1466
1508
|
async function searchWorkspace(root, paths, options) {
|
|
1467
|
-
|
|
1468
|
-
if (!query) throw new TypeError("search query must be a non-empty string");
|
|
1509
|
+
options.signal?.throwIfAborted();
|
|
1510
|
+
if (!options.query) throw new TypeError("search query must be a non-empty string");
|
|
1469
1511
|
const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
1470
1512
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
1471
1513
|
const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
|
|
1514
|
+
if (scoped.length === 0) return [];
|
|
1515
|
+
try {
|
|
1516
|
+
return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
options.signal?.throwIfAborted();
|
|
1519
|
+
return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
var MAX_NATIVE_ARG_BYTES = 96 * 1024;
|
|
1523
|
+
async function nativeSearch(root, paths, options) {
|
|
1524
|
+
const batches = [];
|
|
1525
|
+
let batch = [];
|
|
1526
|
+
let bytes = 0;
|
|
1527
|
+
for (const path of paths) {
|
|
1528
|
+
const size = Buffer.byteLength(path) + 1;
|
|
1529
|
+
if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
|
|
1530
|
+
batches.push(batch);
|
|
1531
|
+
batch = [];
|
|
1532
|
+
bytes = 0;
|
|
1533
|
+
}
|
|
1534
|
+
batch.push(path);
|
|
1535
|
+
bytes += size;
|
|
1536
|
+
}
|
|
1537
|
+
if (batch.length > 0) batches.push(batch);
|
|
1538
|
+
const matches = [];
|
|
1539
|
+
for (const files of batches) {
|
|
1540
|
+
const remaining = options.maxResults - matches.length;
|
|
1541
|
+
if (remaining <= 0) break;
|
|
1542
|
+
matches.push(...await nativeSearchBatch(root, files, options, remaining));
|
|
1543
|
+
}
|
|
1544
|
+
return matches;
|
|
1545
|
+
}
|
|
1546
|
+
function nativeSearchBatch(root, paths, options, remaining) {
|
|
1547
|
+
return new Promise((resolveMatches, reject) => {
|
|
1548
|
+
const args = [
|
|
1549
|
+
"--fixed-strings",
|
|
1550
|
+
"--json",
|
|
1551
|
+
"--no-messages",
|
|
1552
|
+
"--sort=path",
|
|
1553
|
+
`--max-filesize=${options.maxFileBytes}`,
|
|
1554
|
+
"--max-columns=4096",
|
|
1555
|
+
"--max-columns-preview",
|
|
1556
|
+
options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
|
|
1557
|
+
"--",
|
|
1558
|
+
options.query,
|
|
1559
|
+
...paths
|
|
1560
|
+
];
|
|
1561
|
+
const child = spawn3("rg", args, {
|
|
1562
|
+
cwd: root,
|
|
1563
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1564
|
+
...options.signal ? { signal: options.signal } : {}
|
|
1565
|
+
});
|
|
1566
|
+
const matches = [];
|
|
1567
|
+
let carry = "";
|
|
1568
|
+
let stopped = false;
|
|
1569
|
+
const consume = (line) => {
|
|
1570
|
+
if (matches.length >= remaining) return;
|
|
1571
|
+
let event;
|
|
1572
|
+
try {
|
|
1573
|
+
event = JSON.parse(line);
|
|
1574
|
+
} catch {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const path = event.data?.path?.text;
|
|
1578
|
+
const lineNumber = event.data?.line_number;
|
|
1579
|
+
const source = event.data?.lines?.text;
|
|
1580
|
+
if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
|
|
1581
|
+
matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
|
|
1582
|
+
if (matches.length >= remaining) {
|
|
1583
|
+
stopped = true;
|
|
1584
|
+
child.kill();
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
child.stdout.setEncoding("utf8");
|
|
1588
|
+
child.stdout.on("data", (chunk) => {
|
|
1589
|
+
carry += chunk;
|
|
1590
|
+
let newline = carry.indexOf("\n");
|
|
1591
|
+
while (newline >= 0) {
|
|
1592
|
+
consume(carry.slice(0, newline));
|
|
1593
|
+
carry = carry.slice(newline + 1);
|
|
1594
|
+
newline = carry.indexOf("\n");
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
child.once("error", reject);
|
|
1598
|
+
child.once("close", (code) => {
|
|
1599
|
+
if (carry) consume(carry);
|
|
1600
|
+
if (stopped || code === 0 || code === 1) resolveMatches(matches);
|
|
1601
|
+
else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
async function fallbackSearch(root, scoped, options) {
|
|
1606
|
+
const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
|
|
1472
1607
|
const matches = [];
|
|
1473
1608
|
for (const path of scoped) {
|
|
1474
|
-
|
|
1609
|
+
options.signal?.throwIfAborted();
|
|
1610
|
+
if (matches.length >= options.maxResults) break;
|
|
1475
1611
|
let source;
|
|
1476
1612
|
try {
|
|
1477
1613
|
source = await readFile2(resolve4(root, path));
|
|
1478
1614
|
} catch {
|
|
1479
1615
|
continue;
|
|
1480
1616
|
}
|
|
1481
|
-
if (source.byteLength > maxFileBytes || source.includes(0)) continue;
|
|
1617
|
+
if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
|
|
1482
1618
|
const lines = source.toString("utf8").split("\n");
|
|
1483
1619
|
for (let index = 0; index < lines.length; index += 1) {
|
|
1484
1620
|
const raw = lines[index];
|
|
1485
1621
|
const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
|
|
1486
1622
|
if (!haystack.includes(query)) continue;
|
|
1487
1623
|
matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
|
|
1488
|
-
if (matches.length >= maxResults) break;
|
|
1624
|
+
if (matches.length >= options.maxResults) break;
|
|
1489
1625
|
}
|
|
1490
1626
|
}
|
|
1491
1627
|
return matches;
|
|
@@ -1770,6 +1906,9 @@ function workspaceGraphs(workspaceDir, paths) {
|
|
|
1770
1906
|
cache.set(workspaceDir, built);
|
|
1771
1907
|
return built;
|
|
1772
1908
|
}
|
|
1909
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
1910
|
+
cache.delete(workspaceDir);
|
|
1911
|
+
}
|
|
1773
1912
|
var shortId = (id) => id.slice(id.indexOf(":") + 1);
|
|
1774
1913
|
function renderOverview(graphs, prefix) {
|
|
1775
1914
|
const rows = rollup(graphs.graph, FILE, prefix === void 0 ? {} : { prefix });
|
|
@@ -1816,7 +1955,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1816
1955
|
"sandbox.who_imports",
|
|
1817
1956
|
"sandbox.who_touches"
|
|
1818
1957
|
]);
|
|
1819
|
-
async function read(context, request, options, policy) {
|
|
1958
|
+
async function read(context, request, options, policy, registry) {
|
|
1820
1959
|
exactKeys(request.input, ["path", "startLine", "endLine"]);
|
|
1821
1960
|
const path = stringField(request.input, "path");
|
|
1822
1961
|
const startLine = optionalInteger(request.input.startLine) ?? 1;
|
|
@@ -1824,7 +1963,7 @@ async function read(context, request, options, policy) {
|
|
|
1824
1963
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
1825
1964
|
throw new TypeError("requested line range exceeds its bound");
|
|
1826
1965
|
}
|
|
1827
|
-
const paths = await
|
|
1966
|
+
const paths = await registry.files(context.workspaceDir);
|
|
1828
1967
|
if (!paths.includes(path)) {
|
|
1829
1968
|
throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
|
|
1830
1969
|
}
|
|
@@ -1844,13 +1983,13 @@ async function read(context, request, options, policy) {
|
|
|
1844
1983
|
}
|
|
1845
1984
|
return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
1846
1985
|
}
|
|
1847
|
-
async function list(context, request, options, policy) {
|
|
1986
|
+
async function list(context, request, options, policy, registry) {
|
|
1848
1987
|
exactKeys(request.input, ["prefix", "maxEntries"]);
|
|
1849
1988
|
const raw = request.input.prefix;
|
|
1850
1989
|
const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
1851
1990
|
const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
|
|
1852
1991
|
if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
|
|
1853
|
-
const paths = await
|
|
1992
|
+
const paths = await registry.files(context.workspaceDir);
|
|
1854
1993
|
const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
|
|
1855
1994
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
1856
1995
|
const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
|
|
@@ -1868,7 +2007,7 @@ async function list(context, request, options, policy) {
|
|
|
1868
2007
|
{ count: entries.length, truncated }
|
|
1869
2008
|
);
|
|
1870
2009
|
}
|
|
1871
|
-
async function search(context, request, options, policy) {
|
|
2010
|
+
async function search(context, request, options, policy, registry) {
|
|
1872
2011
|
exactKeys(request.input, ["query", "prefix", "maxResults", "caseSensitive"]);
|
|
1873
2012
|
const query = stringField(request.input, "query");
|
|
1874
2013
|
if (query.length > 512) throw new TypeError("search query exceeds its bound");
|
|
@@ -1877,21 +2016,22 @@ async function search(context, request, options, policy) {
|
|
|
1877
2016
|
const maxResults = optionalInteger(request.input.maxResults) ?? 100;
|
|
1878
2017
|
if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
|
|
1879
2018
|
const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
|
|
1880
|
-
const paths = await
|
|
2019
|
+
const paths = await registry.files(context.workspaceDir);
|
|
1881
2020
|
const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
|
|
1882
2021
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
1883
2022
|
const matches = await searchWorkspace(context.workspaceDir, paths, {
|
|
1884
2023
|
query,
|
|
1885
2024
|
maxResults,
|
|
1886
2025
|
caseSensitive,
|
|
1887
|
-
...prefix ? { prefix } : {}
|
|
2026
|
+
...prefix ? { prefix } : {},
|
|
2027
|
+
...context.signal ? { signal: context.signal } : {}
|
|
1888
2028
|
});
|
|
1889
2029
|
if (!matches.length) return response(request, true, `No match for "${query}".`, { count: 0 });
|
|
1890
2030
|
return response(request, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
|
|
1891
2031
|
count: matches.length
|
|
1892
2032
|
});
|
|
1893
2033
|
}
|
|
1894
|
-
async function graphQuery(context, request, options, policy) {
|
|
2034
|
+
async function graphQuery(context, request, options, policy, registry) {
|
|
1895
2035
|
exactKeys(request.input, ["query"]);
|
|
1896
2036
|
const raw = request.input.query;
|
|
1897
2037
|
const query = typeof raw === "string" ? raw : "";
|
|
@@ -1901,7 +2041,7 @@ async function graphQuery(context, request, options, policy) {
|
|
|
1901
2041
|
selector: query
|
|
1902
2042
|
}));
|
|
1903
2043
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
1904
|
-
const paths = await
|
|
2044
|
+
const paths = await registry.files(context.workspaceDir);
|
|
1905
2045
|
const graphs = await workspaceGraphs(context.workspaceDir, paths);
|
|
1906
2046
|
if (request.tool === "sandbox.overview") {
|
|
1907
2047
|
return response(request, true, renderOverview(graphs, query || void 0));
|
|
@@ -1917,23 +2057,38 @@ function createCodeToolBroker(options) {
|
|
|
1917
2057
|
validateOptions(options);
|
|
1918
2058
|
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
1919
2059
|
const policy = createCodePolicyGate(options);
|
|
1920
|
-
|
|
2060
|
+
const registry = createWorkspaceFileRegistry();
|
|
2061
|
+
let barrier = Promise.resolve();
|
|
2062
|
+
const activeReads = /* @__PURE__ */ new Set();
|
|
1921
2063
|
return {
|
|
1922
2064
|
execute(context, request) {
|
|
1923
|
-
|
|
1924
|
-
|
|
2065
|
+
if (isReadTool(request.tool)) {
|
|
2066
|
+
const result2 = barrier.then(() => route(context, request, options, recipes, policy, registry));
|
|
2067
|
+
const settled = result2.then(() => void 0, () => void 0);
|
|
2068
|
+
activeReads.add(settled);
|
|
2069
|
+
void settled.then(() => {
|
|
2070
|
+
activeReads.delete(settled);
|
|
2071
|
+
});
|
|
2072
|
+
return result2;
|
|
2073
|
+
}
|
|
2074
|
+
const earlierReads = [...activeReads];
|
|
2075
|
+
const result = barrier.then(() => Promise.all(earlierReads)).then(() => route(context, request, options, recipes, policy, registry));
|
|
2076
|
+
barrier = result.then(() => void 0, () => void 0);
|
|
1925
2077
|
return result;
|
|
1926
2078
|
}
|
|
1927
2079
|
};
|
|
1928
2080
|
}
|
|
1929
|
-
|
|
2081
|
+
function isReadTool(tool) {
|
|
2082
|
+
return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
|
|
2083
|
+
}
|
|
2084
|
+
async function route(context, request, options, recipes, policy, registry) {
|
|
1930
2085
|
try {
|
|
1931
2086
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
1932
|
-
if (request.tool === "sandbox.read") return await read(context, request, options, policy);
|
|
1933
|
-
if (request.tool === "sandbox.list") return await list(context, request, options, policy);
|
|
1934
|
-
if (request.tool === "sandbox.search") return await search(context, request, options, policy);
|
|
1935
|
-
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy);
|
|
1936
|
-
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
|
|
2087
|
+
if (request.tool === "sandbox.read") return await read(context, request, options, policy, registry);
|
|
2088
|
+
if (request.tool === "sandbox.list") return await list(context, request, options, policy, registry);
|
|
2089
|
+
if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
|
|
2090
|
+
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
|
|
2091
|
+
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
|
|
1937
2092
|
return await recipe(context, request, options, recipes, policy);
|
|
1938
2093
|
} catch (reason) {
|
|
1939
2094
|
return response(request, false, toolFailureMessage(reason));
|
|
@@ -1948,7 +2103,7 @@ function toolFailureMessage(reason) {
|
|
|
1948
2103
|
if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
|
|
1949
2104
|
return "tool failed closed";
|
|
1950
2105
|
}
|
|
1951
|
-
async function patch(context, request, options, policy) {
|
|
2106
|
+
async function patch(context, request, options, policy, registry) {
|
|
1952
2107
|
exactKeys(request.input, ["patch"]);
|
|
1953
2108
|
const value = stringField(request.input, "patch");
|
|
1954
2109
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
@@ -1958,6 +2113,8 @@ async function patch(context, request, options, policy) {
|
|
|
1958
2113
|
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
1959
2114
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
1960
2115
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
2116
|
+
registry.invalidate(context.workspaceDir);
|
|
2117
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
1961
2118
|
return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
1962
2119
|
}
|
|
1963
2120
|
async function recipe(context, request, options, recipes, policy) {
|
|
@@ -2349,7 +2506,7 @@ var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value
|
|
|
2349
2506
|
var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
|
|
2350
2507
|
|
|
2351
2508
|
// src/code-runtime-engine.ts
|
|
2352
|
-
var
|
|
2509
|
+
var TheseusRuntimeEngine = class {
|
|
2353
2510
|
constructor(options) {
|
|
2354
2511
|
this.options = options;
|
|
2355
2512
|
this.#attempt = options.runAgentAttempt ?? runCodeAgentAttempt;
|
|
@@ -2435,7 +2592,7 @@ var CodePiRuntimeEngine = class {
|
|
|
2435
2592
|
await this.#failure(command, active, detail);
|
|
2436
2593
|
return null;
|
|
2437
2594
|
});
|
|
2438
|
-
return { status: "running", message: resume ? "
|
|
2595
|
+
return { status: "running", message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started" };
|
|
2439
2596
|
}
|
|
2440
2597
|
/**
|
|
2441
2598
|
* Pursue a goal: attempt, judge with the clean verifier, re-prompt from what
|
|
@@ -2523,7 +2680,7 @@ var CodePiRuntimeEngine = class {
|
|
|
2523
2680
|
await this.#failure(command, active, detail);
|
|
2524
2681
|
return null;
|
|
2525
2682
|
});
|
|
2526
|
-
return { status: "running", message: "
|
|
2683
|
+
return { status: "running", message: "Theseus accepted the owner prompt" };
|
|
2527
2684
|
}
|
|
2528
2685
|
async #runAttempt(command, metadata, active) {
|
|
2529
2686
|
const lease = fakeCodeLease(command, metadata);
|
|
@@ -2548,7 +2705,8 @@ var CodePiRuntimeEngine = class {
|
|
|
2548
2705
|
lease,
|
|
2549
2706
|
workspaceDir: active.workspace.workspaceDir,
|
|
2550
2707
|
prompt: metadata.prompt,
|
|
2551
|
-
signal: active.abort.signal
|
|
2708
|
+
signal: active.abort.signal,
|
|
2709
|
+
recipeIds: this.options.recipes.map((recipe2) => recipe2.id)
|
|
2552
2710
|
});
|
|
2553
2711
|
const closing = result.finalText.trim();
|
|
2554
2712
|
const completed = result.status === "completed" && Boolean(closing);
|
|
@@ -2612,7 +2770,7 @@ var CodePiRuntimeEngine = class {
|
|
|
2612
2770
|
}
|
|
2613
2771
|
}
|
|
2614
2772
|
async #diagnostic(command, active, value) {
|
|
2615
|
-
const detail = value.trim().slice(0, 2e3) || "
|
|
2773
|
+
const detail = value.trim().slice(0, 2e3) || "Theseus runtime failed";
|
|
2616
2774
|
this.options.onDiagnostic?.(detail);
|
|
2617
2775
|
await this.#event(
|
|
2618
2776
|
command,
|
|
@@ -2666,6 +2824,6 @@ export {
|
|
|
2666
2824
|
renderMemories,
|
|
2667
2825
|
hazardFromAttempt,
|
|
2668
2826
|
runGoal,
|
|
2669
|
-
|
|
2827
|
+
TheseusRuntimeEngine
|
|
2670
2828
|
};
|
|
2671
|
-
//# sourceMappingURL=chunk-
|
|
2829
|
+
//# sourceMappingURL=chunk-GYWQM76X.js.map
|