hammerkit 1.27.0-next.3 → 1.27.0-next.30
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/build.schema.json +188 -0
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +10 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +4 -1
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +168 -17
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/await-running-state.js +59 -13
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +35 -12
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/main.js +2 -0
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +19 -1
- package/dist/planner/utils/append-work-task.js +18 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +124 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +31 -10
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +30 -7
- package/dist/remote/remote-work-commands.js +426 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +8 -1
- package/dist/schema/build-file-container-service-schema.js +12 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +120 -11
- package/dist/schema/schema-parser.js +92 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +739 -0
- package/dist/server/executor.js +57 -5
- package/dist/server/kubernetes-backend.js +367 -65
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +188 -103
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +245 -12
- package/dist/server/runs.js +410 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +255 -14
- package/dist/server/server.js +269 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.executeWorkTask = void 0;
|
|
4
|
+
const work_task_1 = require("../planner/work-task");
|
|
4
5
|
const states_1 = require("./states");
|
|
5
6
|
const abort_1 = require("./abort");
|
|
6
7
|
const log_1 = require("../log");
|
|
@@ -10,9 +11,11 @@ const deadline_1 = require("./deadline");
|
|
|
10
11
|
const explain_1 = require("../cache/explain");
|
|
11
12
|
const archive_task_entry_1 = require("./archive-task-entry");
|
|
12
13
|
const units_1 = require("../utils/units");
|
|
14
|
+
const deferred_restore_1 = require("./deferred-restore");
|
|
13
15
|
async function pushToBackend(work, environment, cacheState, options) {
|
|
14
16
|
const { resolved, stateKey, provable } = cacheState;
|
|
15
|
-
|
|
17
|
+
// an aggregate has nothing to store, see checkCacheState
|
|
18
|
+
if (resolved.method === 'none' || !provable || (0, work_task_1.isAggregateTask)(work.data)) {
|
|
16
19
|
return;
|
|
17
20
|
}
|
|
18
21
|
if (options.cacheReadOnly) {
|
|
@@ -29,7 +32,7 @@ async function pushToBackend(work, environment, cacheState, options) {
|
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
// A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
|
|
32
|
-
//
|
|
35
|
+
// It is never a cache hit itself (see checkCacheState), so its dependencies are
|
|
33
36
|
// always checked and their outputs restored. When every one of them was a hit,
|
|
34
37
|
// nothing ran: report the group as cached too. Reporting only, the cache
|
|
35
38
|
// decision is untouched.
|
|
@@ -87,6 +90,10 @@ dependents = null) {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
|
|
93
|
+
// an aggregate runs nothing, so it needs no outputs on disk
|
|
94
|
+
if (!(0, work_task_1.isAggregateTask)(work.data)) {
|
|
95
|
+
await (0, deferred_restore_1.restoreDeferred)(work, (0, await_completed_dependencies_1.transitiveDeps)(work.deps), abort);
|
|
96
|
+
}
|
|
90
97
|
await (0, await_completed_dependencies_1.awaitRunningNeeds)(work, work.needs.map((n) => n.service), abort);
|
|
91
98
|
await options.processManager.task(work, async () => {
|
|
92
99
|
work.state.set({
|
|
@@ -94,7 +101,7 @@ dependents = null) {
|
|
|
94
101
|
stateKey: cacheState.stateKey,
|
|
95
102
|
started,
|
|
96
103
|
});
|
|
97
|
-
const timeout = work.data.timeout
|
|
104
|
+
const timeout = (0, deadline_1.getTaskTimeout)(work.data.timeout, options);
|
|
98
105
|
const deadline = (0, deadline_1.withDeadline)(abort, timeout);
|
|
99
106
|
try {
|
|
100
107
|
await work.runtime.execute(environment, {
|
|
@@ -9,6 +9,7 @@ const work_task_1 = require("../planner/work-task");
|
|
|
9
9
|
const work_service_1 = require("../planner/work-service");
|
|
10
10
|
const get_container_binds_1 = require("./get-container-binds");
|
|
11
11
|
const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
|
|
12
|
+
const log_1 = require("../log");
|
|
12
13
|
// The files a server run works on: its checkout, or a sandbox. Somebody else wrote them.
|
|
13
14
|
function hostedRoot(environment) {
|
|
14
15
|
return environment.hosted ? environment.cwd : undefined;
|
|
@@ -66,10 +67,27 @@ async function prepareMounts(item, environment) {
|
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
if ((0, path_1.extname)(mount.localPath)) {
|
|
69
|
-
|
|
70
|
+
try {
|
|
71
|
+
await environment.file.writeFile(mount.localPath, '');
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// Docker would make a directory there
|
|
75
|
+
throw new Error(`mount ${mount.localPath} does not exist and can't be created (${(0, log_1.getErrorMessage)(e)})`);
|
|
76
|
+
}
|
|
70
77
|
}
|
|
71
78
|
else {
|
|
72
|
-
|
|
79
|
+
try {
|
|
80
|
+
await environment.file.createDirectory(mount.localPath);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// on a server a root-owned directory in the run's files could break its cleanup
|
|
84
|
+
if (environment.hosted) {
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
// hammerkit may run in a container whose file system is not the one of the Docker
|
|
88
|
+
// host: Docker creates the missing directory of a bind on its own
|
|
89
|
+
item.status.write('warn', `can't create ${mount.localPath} (${(0, log_1.getErrorMessage)(e)}), Docker creates it on its host as root`);
|
|
90
|
+
}
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
93
|
if ((0, work_task_1.isContainerWorkTask)(item.data)) {
|
|
@@ -89,8 +107,11 @@ exports.prepareMounts = prepareMounts;
|
|
|
89
107
|
async function setUserPermissions(item, container, environment) {
|
|
90
108
|
if (item.data.user) {
|
|
91
109
|
// On a server only the volumes: a chown as the container's root on a path of the host
|
|
92
|
-
// would follow a link a task put there after the binds were checked
|
|
93
|
-
|
|
110
|
+
// would follow a link a task put there after the binds were checked. Never a mount: it
|
|
111
|
+
// is a path of the host the build file names, such as /var/run/docker.sock, whose owner
|
|
112
|
+
// is not hammerkit's to change
|
|
113
|
+
const mounted = new Set(item.data.mounts.map((mount) => mount.containerPath));
|
|
114
|
+
const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !mounted.has(bind.containerPath) && (!environment.hosted || !(0, path_1.isAbsolute)(bind.localPath)));
|
|
94
115
|
if (!environment.hosted) {
|
|
95
116
|
await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
|
|
96
117
|
}
|
|
@@ -5,6 +5,10 @@ const state_1 = require("./state");
|
|
|
5
5
|
function resetWorkTree(workTree, type) {
|
|
6
6
|
return {
|
|
7
7
|
tasks: Object.entries(workTree.tasks).reduce((tasks, [key, task]) => {
|
|
8
|
+
// down only stops services: a task it selects names the services it needs
|
|
9
|
+
if (type === 'down') {
|
|
10
|
+
return tasks;
|
|
11
|
+
}
|
|
8
12
|
task.state = new state_1.State({
|
|
9
13
|
type: 'pending',
|
|
10
14
|
stateKey: null,
|
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkCacheState = exports.UNCACHED_STATE_KEY = void 0;
|
|
4
4
|
const work_item_1 = require("../../planner/work-item");
|
|
5
|
+
const work_task_1 = require("../../planner/work-task");
|
|
5
6
|
const get_cache_directory_1 = require("../../optimizer/get-cache-directory");
|
|
6
7
|
const cache_metadata_1 = require("../cache-metadata");
|
|
7
8
|
const work_task_cache_description_1 = require("../../optimizer/work-task-cache-description");
|
|
8
9
|
const log_1 = require("../../log");
|
|
9
10
|
const state_key_1 = require("./state-key");
|
|
10
11
|
const work_instance_id_1 = require("../../planner/work-instance-id");
|
|
12
|
+
const deferred_restore_1 = require("../deferred-restore");
|
|
11
13
|
// The key a task whose cache is off runs and records its outputs under. It is
|
|
12
14
|
// no hash, so a later run with its cache on never takes those outputs as its own.
|
|
13
15
|
exports.UNCACHED_STATE_KEY = 'uncached';
|
|
14
16
|
async function checkCacheState(item, defaultCacheMethod, environment,
|
|
15
17
|
// watch mode compares keys to rerun on a change, so it needs one even when
|
|
16
18
|
// the cache is off
|
|
19
|
+
// restore: false only looks a backend hit up, its outputs are downloaded once
|
|
20
|
+
// something that runs depends on them (`run --no-restore`, see deferred-restore)
|
|
17
21
|
options = {}) {
|
|
22
|
+
(0, deferred_restore_1.clearDeferredRestore)(item);
|
|
18
23
|
// A task whose cache is off is never looked up, so hashing its sources buys
|
|
19
24
|
// nothing, and a `**` over a node_modules takes most of a minute (every run
|
|
20
25
|
// with arguments). Its dependents still hash its sources for their own key.
|
|
@@ -35,18 +40,41 @@ options = {}) {
|
|
|
35
40
|
item.status.write('debug', `${item.name} always runs, it or one of its dependencies has no src files`);
|
|
36
41
|
return { cached: false, stateKey, resolved, provable };
|
|
37
42
|
}
|
|
43
|
+
// An aggregate runs nothing, so running it costs nothing, and it is never a hit itself:
|
|
44
|
+
// asked for, it still has its dependencies checked and their outputs restored. Its key
|
|
45
|
+
// counts for what depends on it, and it is reported cached when they all were.
|
|
46
|
+
if ((0, work_item_1.isWorkTaskItem)(item) && (0, work_task_1.isAggregateTask)(item.data)) {
|
|
47
|
+
return { cached: false, stateKey, resolved, provable };
|
|
48
|
+
}
|
|
38
49
|
if ((0, work_item_1.isWorkTaskItem)(item)) {
|
|
39
50
|
const runtimeStateKey = await item.runtime.currentStateKey(environment);
|
|
40
51
|
if (runtimeStateKey === stateKey) {
|
|
41
52
|
return { cached: true, stateKey, resolved, provable };
|
|
42
53
|
}
|
|
54
|
+
if (options.restore === false && options.watch) {
|
|
55
|
+
item.status.write('debug', `${item.name} restores cache hits, --no-restore has no effect in watch mode`);
|
|
56
|
+
}
|
|
57
|
+
if (options.restore === false && !options.watch) {
|
|
58
|
+
try {
|
|
59
|
+
if (await resolved.backend.has(item.id(), stateKey, environment)) {
|
|
60
|
+
item.status.write('info', `${item.name} found in cache "${resolved.name}" (${resolved.backend.type}), not restored`);
|
|
61
|
+
await touchEntry(item, resolved, stateKey, environment);
|
|
62
|
+
(0, deferred_restore_1.deferRestore)(item, async () => {
|
|
63
|
+
if (!(await restoreFromBackend(item, resolved, stateKey, currentStats, environment))) {
|
|
64
|
+
throw new Error(`${item.name} is no longer in cache "${resolved.name}", its outputs can not be restored. ` +
|
|
65
|
+
'Run again without --no-restore.');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return { cached: true, stateKey, resolved, provable };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
item.status.write('warn', `${item.name} failed to look up cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)} — continuing with local execution`);
|
|
73
|
+
}
|
|
74
|
+
return { cached: false, stateKey, resolved, provable };
|
|
75
|
+
}
|
|
43
76
|
try {
|
|
44
|
-
|
|
45
|
-
const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
|
|
46
|
-
if (pulled) {
|
|
47
|
-
item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
|
|
48
|
-
await item.runtime.restore(environment, cacheDir, stateKey);
|
|
49
|
-
await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
|
|
77
|
+
if (await restoreFromBackend(item, resolved, stateKey, currentStats, environment)) {
|
|
50
78
|
return { cached: true, stateKey, resolved, provable };
|
|
51
79
|
}
|
|
52
80
|
}
|
|
@@ -57,4 +85,27 @@ options = {}) {
|
|
|
57
85
|
return { cached: false, stateKey, resolved, provable };
|
|
58
86
|
}
|
|
59
87
|
exports.checkCacheState = checkCacheState;
|
|
88
|
+
// Pull a task's entry and restore its outputs into the checkout. False when the
|
|
89
|
+
// backend has no such entry.
|
|
90
|
+
async function restoreFromBackend(item, resolved, stateKey, currentStats, environment) {
|
|
91
|
+
const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(item));
|
|
92
|
+
const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
|
|
93
|
+
if (!pulled) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
|
|
97
|
+
await item.runtime.restore(environment, cacheDir, stateKey);
|
|
98
|
+
await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
// A hit that is only looked up still counts as used, so retention keeps it.
|
|
102
|
+
// Best effort: the hit stands whether or not the marker could be written.
|
|
103
|
+
async function touchEntry(item, resolved, stateKey, environment) {
|
|
104
|
+
try {
|
|
105
|
+
await resolved.backend.touch?.(item.id(), stateKey, environment);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
item.status.write('debug', `${item.name} could not mark its cache entry as used: ${(0, log_1.getErrorMessage)(e)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
60
111
|
//# sourceMappingURL=enqueue-next.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.computeStateKey = exports.combineStateKeys = exports.resolveEffective = void 0;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
3
|
+
exports.computeStateKey = exports.combineStateKeys = exports.isOwnProvable = exports.resolveEffective = void 0;
|
|
5
4
|
const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
|
|
5
|
+
const work_task_1 = require("../../planner/work-task");
|
|
6
6
|
// The state-key engine is shared by the scheduler's hit/miss decision
|
|
7
7
|
// (`checkCacheState`) and the read-only cache-explain engine, so a prediction
|
|
8
8
|
// can never diverge from the decision it predicts — there is one implementation.
|
|
@@ -21,9 +21,14 @@ exports.resolveEffective = resolveEffective;
|
|
|
21
21
|
// whose `src` entries all match no file (a typo, a moved path). Services are not
|
|
22
22
|
// affected.
|
|
23
23
|
function isOwnProvable(item, unmatched) {
|
|
24
|
+
if ((0, work_task_1.isAggregateTask)(item.data)) {
|
|
25
|
+
// it runs nothing and reads nothing: what its dependencies prove is all there is
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
24
28
|
// unmatched ⊆ declared, so this is also false when nothing is declared
|
|
25
29
|
return unmatched.length < item.data.src.filter((src) => !src.inherited).length;
|
|
26
30
|
}
|
|
31
|
+
exports.isOwnProvable = isOwnProvable;
|
|
27
32
|
// Fold a task's own state key together with the state keys of its dependencies.
|
|
28
33
|
// A change anywhere in the dependency subtree must change the resulting key, so
|
|
29
34
|
// that a downstream task is invalidated when an upstream source changes — even
|
|
@@ -34,9 +39,7 @@ function combineStateKeys(ownKey, depKeys) {
|
|
|
34
39
|
}
|
|
35
40
|
// Sort so the key is independent of dependency declaration order.
|
|
36
41
|
const sorted = [...depKeys].sort();
|
|
37
|
-
return (0,
|
|
38
|
-
.update([ownKey, ...sorted].join(','))
|
|
39
|
-
.digest('hex');
|
|
42
|
+
return (0, get_work_cache_stats_1.stateKeyDigest)([ownKey, ...sorted].join(','));
|
|
40
43
|
}
|
|
41
44
|
exports.combineStateKeys = combineStateKeys;
|
|
42
45
|
// `none` only disables the cache *check* for a task; its sources still influence
|
|
@@ -72,7 +75,41 @@ async function computeStateKey(item, defaultCacheMethod, environment, memo = new
|
|
|
72
75
|
depKeys.push(effective.stateKey);
|
|
73
76
|
provable = provable && effective.provable;
|
|
74
77
|
}
|
|
78
|
+
// what a task that needs a service passed against: the sources of the service and of the
|
|
79
|
+
// tasks it depends on (its definition is in the task id, getWorkServiceCacheIdentity)
|
|
80
|
+
for (const need of item.needs) {
|
|
81
|
+
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
|
|
82
|
+
depKeys.push(`${need.name}=${effective.stateKey}`);
|
|
83
|
+
}
|
|
75
84
|
return { stateKey: combineStateKeys(ownKey, depKeys), stats, resolved, provable, unmatched };
|
|
76
85
|
}
|
|
77
86
|
exports.computeStateKey = computeStateKey;
|
|
87
|
+
// A needed service's sources folded with the state keys of its dependencies and needs. It
|
|
88
|
+
// does not make the task that needs it unprovable: a dependency without `src` (resetting the
|
|
89
|
+
// service's data, say) is skipped with the service when the task is a cache hit.
|
|
90
|
+
async function resolveNeedStateKey(service, defaultCacheMethod, environment, memo) {
|
|
91
|
+
const memoKey = `need\0${service.name}`;
|
|
92
|
+
const existing = memo.get(memoKey);
|
|
93
|
+
if (existing !== undefined) {
|
|
94
|
+
return existing;
|
|
95
|
+
}
|
|
96
|
+
// services that need each other in a loop fail to plan (checkForLoop): this only has to end
|
|
97
|
+
memo.set(memoKey, { stateKey: 'loop', provable: true });
|
|
98
|
+
const resolved = resolveEffective(service, defaultCacheMethod);
|
|
99
|
+
const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(service, environment);
|
|
100
|
+
const keys = [];
|
|
101
|
+
for (const dep of service.deps) {
|
|
102
|
+
keys.push((await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo)).stateKey);
|
|
103
|
+
}
|
|
104
|
+
for (const need of service.needs) {
|
|
105
|
+
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
|
|
106
|
+
keys.push(`${need.name}=${effective.stateKey}`);
|
|
107
|
+
}
|
|
108
|
+
const effective = {
|
|
109
|
+
stateKey: combineStateKeys((0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod)), keys),
|
|
110
|
+
provable: true,
|
|
111
|
+
};
|
|
112
|
+
memo.set(memoKey, effective);
|
|
113
|
+
return effective;
|
|
114
|
+
}
|
|
78
115
|
//# sourceMappingURL=state-key.js.map
|
|
@@ -12,7 +12,10 @@ async function watchLoop(work, environment, options, factory) {
|
|
|
12
12
|
// kubernetes-service has no such field. Read it wherever it exists.
|
|
13
13
|
const continuous = 'continuous' in work.data ? work.data.continuous : false;
|
|
14
14
|
const watchMode = options.watch && !options.daemon && !continuous;
|
|
15
|
-
const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, {
|
|
15
|
+
const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, {
|
|
16
|
+
watch: watchMode,
|
|
17
|
+
restore: options.restore,
|
|
18
|
+
});
|
|
16
19
|
(0, abort_1.checkForAbort)(environment.abortCtrl.signal);
|
|
17
20
|
const watchState = watchMode
|
|
18
21
|
? (0, watch_state_key_1.watchStateKey)(work, currentStats, environment, options)
|
package/dist/harness/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getHarnessEnvironment = exports.connectSandbox = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
|
|
3
|
+
exports.getHarnessEnvironment = exports.connectSandbox = exports.connectServer = exports.hasShell = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
|
|
4
4
|
const get_file_context_1 = require("../file/get-file-context");
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const work_item_status_1 = require("../planner/work-item-status");
|
|
@@ -20,11 +20,21 @@ function harnessOptionsFromEnv(env) {
|
|
|
20
20
|
repo: value('HAMMERKIT_REPO'),
|
|
21
21
|
ref: value('HAMMERKIT_REF'),
|
|
22
22
|
build: value('HAMMERKIT_BUILD'),
|
|
23
|
-
shell
|
|
23
|
+
// set but empty is no shell, unset the default one
|
|
24
|
+
shell: env.HAMMERKIT_SHELL !== undefined ? env.HAMMERKIT_SHELL.trim() : undefined,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
exports.harnessOptionsFromEnv = harnessOptionsFromEnv;
|
|
27
|
-
|
|
28
|
+
// whether the clients offer a shell tool
|
|
29
|
+
function hasShell(options) {
|
|
30
|
+
return options.shell !== '';
|
|
31
|
+
}
|
|
32
|
+
exports.hasShell = hasShell;
|
|
33
|
+
/**
|
|
34
|
+
* The registered server a harness works on and the published version it runs, checked:
|
|
35
|
+
* what the clients need to list the tasks of the version before they take a sandbox.
|
|
36
|
+
*/
|
|
37
|
+
async function connectServer(environment, options) {
|
|
28
38
|
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
29
39
|
const name = options.server ?? config.run.on;
|
|
30
40
|
if (!name || !(0, host_config_1.hasServer)(config.servers, name)) {
|
|
@@ -39,8 +49,12 @@ async function connectSandbox(environment, options, log = () => undefined) {
|
|
|
39
49
|
if (!bundle_1.HASH_PATTERN.test(options.build)) {
|
|
40
50
|
throw new Error(`${options.build} is not the hash of a published version (sha256:...), name it as hammerkit remote publish printed it (HAMMERKIT_BUILD or --build)`);
|
|
41
51
|
}
|
|
42
|
-
const server = config
|
|
43
|
-
|
|
52
|
+
const server = (0, host_config_1.getRegisteredServer)(config, name);
|
|
53
|
+
return { name, connection: { url: server.url, ...(0, token_source_1.getTokenSource)(environment, name, server) }, build: options.build };
|
|
54
|
+
}
|
|
55
|
+
exports.connectServer = connectServer;
|
|
56
|
+
async function connectSandbox(environment, options, log = () => undefined) {
|
|
57
|
+
const { name, connection, build } = await connectServer(environment, options);
|
|
44
58
|
let sandbox;
|
|
45
59
|
if (options.sandbox) {
|
|
46
60
|
sandbox = await sandbox_client_1.SandboxClient.get(connection, options.sandbox);
|
|
@@ -59,7 +73,7 @@ async function connectSandbox(environment, options, log = () => undefined) {
|
|
|
59
73
|
else {
|
|
60
74
|
throw new Error('name a sandbox (HAMMERKIT_SANDBOX or --sandbox), or a repository and ref to create one from (HAMMERKIT_REPO and HAMMERKIT_REF, or --repo and --ref)');
|
|
61
75
|
}
|
|
62
|
-
return new sandbox_client_1.SandboxClient(connection, sandbox, { hash:
|
|
76
|
+
return new sandbox_client_1.SandboxClient(connection, sandbox, { hash: build, shell: options.shell ?? exports.DEFAULT_SHELL_TASK });
|
|
63
77
|
}
|
|
64
78
|
exports.connectSandbox = connectSandbox;
|
|
65
79
|
// A client that starts with a repository and ref takes a sandbox of the caller's of the same
|
|
@@ -6,6 +6,15 @@ const log_1 = require("../log");
|
|
|
6
6
|
const version_1 = require("../version");
|
|
7
7
|
const connect_1 = require("./connect");
|
|
8
8
|
const mcp_1 = require("./mcp");
|
|
9
|
+
const task_tools_1 = require("./task-tools");
|
|
10
|
+
// the tasks run_task lists; a server that does not answer leaves them out, the tools still work
|
|
11
|
+
async function listTasks(sandbox, log) {
|
|
12
|
+
const listing = await (0, task_tools_1.readTaskListing)(() => sandbox.tasks());
|
|
13
|
+
if (!Array.isArray(listing)) {
|
|
14
|
+
log(`the tasks of ${sandbox.build.hash} could not be listed: ${listing.error}`);
|
|
15
|
+
}
|
|
16
|
+
return listing;
|
|
17
|
+
}
|
|
9
18
|
// `hammerkit remote mcp`: a Model Context Protocol server on stdio for one sandbox of a
|
|
10
19
|
// server. It lives under `remote`, which already is a command: a top-level `mcp` would hide a
|
|
11
20
|
// task named `mcp`, since `hammerkit <task>` runs a task. Like the other `remote` commands it
|
|
@@ -21,20 +30,27 @@ function registerMcpCommand(remote, environment, input = process.stdin) {
|
|
|
21
30
|
.addOption(new commander_1.Option('--repo <repo>', 'create a sandbox of this repository').env('HAMMERKIT_REPO'))
|
|
22
31
|
.addOption(new commander_1.Option('--ref <ref>', 'at this branch').env('HAMMERKIT_REF'))
|
|
23
32
|
.addOption(new commander_1.Option('--build <hash>', 'hash of the published version of the build file whose tasks run on it').env('HAMMERKIT_BUILD'))
|
|
24
|
-
.addOption(new commander_1.Option('--shell <task>',
|
|
33
|
+
.addOption(new commander_1.Option('--shell <task>', "its task whose last command is a shell, like sh -c; '' for no bash tool")
|
|
25
34
|
.env('HAMMERKIT_SHELL')
|
|
26
35
|
.default(connect_1.DEFAULT_SHELL_TASK))
|
|
27
36
|
.action(async (options) => {
|
|
28
37
|
try {
|
|
29
38
|
// stdout carries the protocol only, everything else goes to stderr
|
|
30
39
|
const log = (line) => environment.stderr.write(`hammerkit remote mcp: ${line}\n`);
|
|
31
|
-
|
|
40
|
+
// as the plugins read it: blank is no shell
|
|
41
|
+
const sandbox = await (0, connect_1.connectSandbox)(environment, { ...options, shell: options.shell.trim() }, log);
|
|
32
42
|
log(`sandbox ${sandbox.id} of ${sandbox.sandbox.repo} at ${sandbox.sandbox.commit}`);
|
|
43
|
+
const shell = sandbox.build.shell;
|
|
33
44
|
const instructions = `The tools work on hammerkit sandbox ${sandbox.id}, a checkout of ${sandbox.sandbox.repo} at ` +
|
|
34
|
-
`${sandbox.sandbox.commit} on a server. Paths are relative to its root.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
`${sandbox.sandbox.commit} on a server. Paths are relative to its root. ` +
|
|
46
|
+
(shell
|
|
47
|
+
? `bash runs a command as the task ${shell} of the published build file ${sandbox.build.hash}, ` +
|
|
48
|
+
'run_task runs its other tasks; only the files are kept between commands. '
|
|
49
|
+
: `run_task runs the tasks of the published build file ${sandbox.build.hash}; only the files are ` +
|
|
50
|
+
'kept between runs. ') +
|
|
51
|
+
'changes shows what changed since the fetch.';
|
|
52
|
+
const tasks = await listTasks(sandbox, log);
|
|
53
|
+
const served = (0, mcp_1.serveMcp)(input, environment.stdout, (0, mcp_1.createMcpTools)(async () => sandbox, { tasks, shell: shell || false }), {
|
|
38
54
|
name: 'hammerkit',
|
|
39
55
|
version: (0, version_1.getVersion)(),
|
|
40
56
|
instructions,
|
package/dist/harness/mcp.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.serveMcp = exports.createMcpTools = void 0;
|
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const format_1 = require("./format");
|
|
6
6
|
const sandbox_client_1 = require("./sandbox-client");
|
|
7
|
+
const task_tools_1 = require("./task-tools");
|
|
8
|
+
const service_tools_1 = require("./service-tools");
|
|
7
9
|
// the newest first; a client asking for an older one gets it
|
|
8
10
|
const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
9
11
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
@@ -25,40 +27,47 @@ const flag = (description) => ({ type: 'boolean', description });
|
|
|
25
27
|
function sandboxPath(path) {
|
|
26
28
|
return (path ?? '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
27
29
|
}
|
|
28
|
-
function readLog(run, task) {
|
|
29
|
-
return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
|
|
30
|
-
}
|
|
31
|
-
function describeRun(task, ran) {
|
|
32
|
-
const result = ran.result;
|
|
33
|
-
const lines = [`run ${ran.run.id}: ${ran.run.state}`];
|
|
34
|
-
if (result) {
|
|
35
|
-
lines.push(`task ${task}: ${result.status}` +
|
|
36
|
-
(result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
|
|
37
|
-
(result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
|
|
38
|
-
(result.cache?.hit ? ', from the cache' : ''));
|
|
39
|
-
if (result.error) {
|
|
40
|
-
lines.push(`error: ${result.error}`);
|
|
41
|
-
}
|
|
42
|
-
if (result.cache?.causes?.length) {
|
|
43
|
-
lines.push(`ran because: ${result.cache.causes.join('; ')}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else if (ran.run.error) {
|
|
47
|
-
lines.push(`error: ${ran.run.error}`);
|
|
48
|
-
}
|
|
49
|
-
if (result && result.lastLog.length > 0) {
|
|
50
|
-
lines.push('last lines:', ...result.lastLog);
|
|
51
|
-
}
|
|
52
|
-
lines.push(readLog(ran.run.id, task));
|
|
53
|
-
return lines.join('\n');
|
|
54
|
-
}
|
|
55
30
|
/**
|
|
56
31
|
* The tools of `hammerkit remote mcp`, for chat clients and agents without file and shell tools of
|
|
57
32
|
* their own. They take the shapes models are trained on: numbered reads with offset and
|
|
58
33
|
* limit, exact-string replace, `apply_patch` or a git diff, ripgrep's parameters, a shell
|
|
59
|
-
* command. Paths are relative to the root of the sandbox.
|
|
34
|
+
* command. Paths are relative to the root of the sandbox. `tasks` are the tasks the description
|
|
35
|
+
* of `run_task` lists; without `shell` there is no `bash`.
|
|
60
36
|
*/
|
|
61
|
-
function createMcpTools(client) {
|
|
37
|
+
function createMcpTools(client, options = {}) {
|
|
38
|
+
const [runTask, runLogs, check] = (0, task_tools_1.createTaskTools)(options.tasks, options.shell || undefined).map((tool) => fromTaskTool(tool, client));
|
|
39
|
+
const bash = {
|
|
40
|
+
name: 'bash',
|
|
41
|
+
description: 'Run a shell command on the sandbox and answer its output and exit code. It runs as the shell task of ' +
|
|
42
|
+
'the published build file, in its container and with its network; commands run one at a time, and ' +
|
|
43
|
+
'only the files are kept between them.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
command: text('The command'),
|
|
48
|
+
workdir: text('The directory to run it in, relative to the root of the sandbox'),
|
|
49
|
+
timeout: count('Stop the command after this many seconds', 1),
|
|
50
|
+
},
|
|
51
|
+
required: ['command'],
|
|
52
|
+
},
|
|
53
|
+
annotations: { title: 'Run a command' },
|
|
54
|
+
async call(args, signal) {
|
|
55
|
+
const sandbox = await client();
|
|
56
|
+
const ran = await sandbox.exec(args.command, sandboxPath(args.workdir), {
|
|
57
|
+
signal,
|
|
58
|
+
timeoutSeconds: args.timeout,
|
|
59
|
+
});
|
|
60
|
+
const output = (0, format_1.capOutput)(ran.output, (0, task_tools_1.readLogHint)(ran.run.id, sandbox.build.shell));
|
|
61
|
+
const lines = output ? [output, ''] : [];
|
|
62
|
+
if (ran.exitCode === null) {
|
|
63
|
+
lines.push(`hammerkit: ${ran.result?.error ?? ran.run.error ?? `the run ended ${ran.run.state}`}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push(`exit code ${ran.exitCode}`);
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
},
|
|
70
|
+
};
|
|
62
71
|
return inOrder([
|
|
63
72
|
{
|
|
64
73
|
name: 'read',
|
|
@@ -185,91 +194,10 @@ function createMcpTools(client) {
|
|
|
185
194
|
return (0, format_1.formatTree)(await (await client()).tree({ path, glob: args.pattern, limit: 500 }));
|
|
186
195
|
},
|
|
187
196
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'only the files are kept between them.',
|
|
193
|
-
inputSchema: {
|
|
194
|
-
type: 'object',
|
|
195
|
-
properties: {
|
|
196
|
-
command: text('The command'),
|
|
197
|
-
workdir: text('The directory to run it in, relative to the root of the sandbox'),
|
|
198
|
-
timeout: count('Stop the command after this many seconds', 1),
|
|
199
|
-
},
|
|
200
|
-
required: ['command'],
|
|
201
|
-
},
|
|
202
|
-
annotations: { title: 'Run a command' },
|
|
203
|
-
async call(args, signal) {
|
|
204
|
-
const sandbox = await client();
|
|
205
|
-
const ran = await sandbox.exec(args.command, sandboxPath(args.workdir), {
|
|
206
|
-
signal,
|
|
207
|
-
timeoutSeconds: args.timeout,
|
|
208
|
-
});
|
|
209
|
-
const output = (0, format_1.capOutput)(ran.output, readLog(ran.run.id, sandbox.build.shell));
|
|
210
|
-
const lines = output ? [output, ''] : [];
|
|
211
|
-
if (ran.exitCode === null) {
|
|
212
|
-
lines.push(`hammerkit: ${ran.result?.error ?? ran.run.error ?? `the run ended ${ran.run.state}`}`);
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
lines.push(`exit code ${ran.exitCode}`);
|
|
216
|
-
}
|
|
217
|
-
return lines.join('\n');
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
name: 'run_task',
|
|
222
|
-
description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
|
|
223
|
-
'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
|
|
224
|
-
'its last command, for example a test file.',
|
|
225
|
-
inputSchema: {
|
|
226
|
-
type: 'object',
|
|
227
|
-
properties: {
|
|
228
|
-
task: text('The name of the task'),
|
|
229
|
-
args: { type: 'array', items: { type: 'string' }, description: 'Arguments for its last command' },
|
|
230
|
-
},
|
|
231
|
-
required: ['task'],
|
|
232
|
-
},
|
|
233
|
-
annotations: { title: 'Run a task' },
|
|
234
|
-
async call(args, signal) {
|
|
235
|
-
const ran = await (await client()).runTask(args.task, { args: args.args, signal });
|
|
236
|
-
return describeRun(args.task, ran);
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'run_logs',
|
|
241
|
-
description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
|
|
242
|
-
'the lines that contain a text (grep, a fixed string) with lines of context around them.',
|
|
243
|
-
inputSchema: {
|
|
244
|
-
type: 'object',
|
|
245
|
-
properties: {
|
|
246
|
-
run: text('The id of the run'),
|
|
247
|
-
task: text('Only the lines of this task'),
|
|
248
|
-
tail: count('The last lines, or with grep the last matches', 1, 1000),
|
|
249
|
-
from: count('The first line to read', 1),
|
|
250
|
-
limit: count('How many lines to read', 1, 1000),
|
|
251
|
-
grep: text('Only lines that contain this text'),
|
|
252
|
-
ignore_case: flag('Ignore case in grep'),
|
|
253
|
-
context: count('Lines before and after each match', 0, 20),
|
|
254
|
-
},
|
|
255
|
-
required: ['run'],
|
|
256
|
-
},
|
|
257
|
-
annotations: { title: 'Read the log of a run', ...READ_ONLY },
|
|
258
|
-
async call(args) {
|
|
259
|
-
const page = await (await client()).logs(args.run, {
|
|
260
|
-
task: args.task,
|
|
261
|
-
tail: args.tail,
|
|
262
|
-
from: args.from,
|
|
263
|
-
limit: args.limit,
|
|
264
|
-
grep: args.grep,
|
|
265
|
-
i: args.ignore_case,
|
|
266
|
-
context: args.context,
|
|
267
|
-
});
|
|
268
|
-
const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
|
|
269
|
-
lines.push('', `(${page.total} lines${args.grep ? ` searched, ${page.lines.filter((line) => line.match !== false).length} match here` : ''}${page.next ? `, read on with from ${page.next}` : ''}${page.truncated ? ', the server cut the log off at its limit' : ''})`);
|
|
270
|
-
return lines.join('\n');
|
|
271
|
-
},
|
|
272
|
-
},
|
|
197
|
+
...(options.shell === false ? [] : [bash]),
|
|
198
|
+
runTask,
|
|
199
|
+
check,
|
|
200
|
+
{ ...runLogs, annotations: { ...runLogs.annotations, ...READ_ONLY } },
|
|
273
201
|
{
|
|
274
202
|
name: 'changes',
|
|
275
203
|
description: 'What changed in the sandbox since the commit it was fetched at: a summary of the files, a diff, or a ' +
|
|
@@ -315,9 +243,30 @@ function createMcpTools(client) {
|
|
|
315
243
|
return `pushed ${pushed.commit} to ${pushed.branch}`;
|
|
316
244
|
},
|
|
317
245
|
},
|
|
246
|
+
...service_tools_1.SERVICE_TOOLS.map((tool) => ({
|
|
247
|
+
name: tool.name,
|
|
248
|
+
description: tool.description,
|
|
249
|
+
inputSchema: (0, service_tools_1.serviceToolSchema)(tool),
|
|
250
|
+
annotations: { title: tool.title, readOnlyHint: tool.name === 'endpoint_get', openWorldHint: false },
|
|
251
|
+
async call(args, signal) {
|
|
252
|
+
return tool.call(await client(), args, signal);
|
|
253
|
+
},
|
|
254
|
+
})),
|
|
318
255
|
]);
|
|
319
256
|
}
|
|
320
257
|
exports.createMcpTools = createMcpTools;
|
|
258
|
+
// a tool shared with the other harnesses, as an MCP tool
|
|
259
|
+
function fromTaskTool(tool, client) {
|
|
260
|
+
return {
|
|
261
|
+
name: tool.name,
|
|
262
|
+
description: tool.description,
|
|
263
|
+
inputSchema: (0, task_tools_1.taskToolSchema)(tool),
|
|
264
|
+
annotations: { title: tool.title },
|
|
265
|
+
async call(args, signal) {
|
|
266
|
+
return tool.call(await client(), args, signal);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
321
270
|
// the calls of a client go to the sandbox one at a time, in the order the client sent them
|
|
322
271
|
function inOrder(tools) {
|
|
323
272
|
const inTurn = (0, sandbox_client_1.oneAtATime)();
|