maka-agent 0.2.0-dev.4.20260830 → 0.2.0-dev.5.20260830
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eval-bundle-path.js +1 -1
- package/dist/pi-tui-runner.js +3 -1
- package/dist/runtime-host-onboarding.js +10 -1
- package/dist/runtime-host-tui-context.js +2 -6
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/node_modules/@maka/eval/dist/external-subject.js +1 -1
- package/node_modules/@maka/eval/dist/harbor-external-subject.js +2 -2
- package/package.json +1 -2
- package/packages/eval/dist/attempt-store.js +0 -138
- package/packages/eval/dist/cli.js +0 -119
- package/packages/eval/dist/experiment-directory.js +0 -59
- package/packages/eval/dist/experiment.js +0 -49
- package/packages/eval/dist/external-subject.js +0 -488
- package/packages/eval/dist/harbor-external-subject.js +0 -929
- package/packages/eval/dist/harbor-maka-subject.js +0 -83
- package/packages/eval/dist/harness-environment.js +0 -80
- package/packages/eval/dist/harness-executor.js +0 -1013
- package/packages/eval/dist/index.js +0 -28
- package/packages/eval/dist/install-preflight.js +0 -201
- package/packages/eval/dist/maka-artifacts.js +0 -126
- package/packages/eval/dist/maka-runtime-policy.js +0 -42
- package/packages/eval/dist/maka-subject.js +0 -268
- package/packages/eval/dist/metering-checkpoint.js +0 -80
- package/packages/eval/dist/provider-admission.js +0 -39
- package/packages/eval/dist/provider-metering.js +0 -67
- package/packages/eval/dist/provider-web-tool-surface.js +0 -74
- package/packages/eval/dist/relay-result-frame.js +0 -38
- package/packages/eval/dist/result.js +0 -94
- package/packages/eval/dist/runner.js +0 -290
- package/packages/eval/dist/spec.js +0 -135
- package/packages/eval/dist/toolchain-verification.js +0 -130
- package/packages/eval/harbor/deepseek-codex-models.json +0 -69
- package/packages/eval/harbor/deepseek-harness-profile/cordis.patch.yml +0 -140
- package/packages/eval/harbor/deepseek-harness-profile/cordis.yml +0 -18
- package/packages/eval/harbor/deepseek-harness-profile/package.json +0 -10
- package/packages/eval/harbor/docker-compose-egress-proxy.yaml +0 -57
- package/packages/eval/harbor/egress-proxy/Dockerfile +0 -25
- package/packages/eval/harbor/egress-proxy/entrypoint.sh +0 -62
- package/packages/eval/harbor/egress-proxy/network-policy +0 -148
- package/packages/eval/harbor/egress_filter.py +0 -435
- package/packages/eval/harbor/eval_framework.py +0 -40
- package/packages/eval/harbor/relay_agent.py +0 -686
- package/packages/eval/harbor/run_trial.py +0 -223
- package/packages/eval/package.json +0 -20
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
20
|
-
import { constants } from 'node:fs';
|
|
21
|
-
import { chmod, open, rename, writeFile } from 'node:fs/promises';
|
|
22
|
-
// One JSON snapshot of proxy counts. Anything larger is not a checkpoint the
|
|
23
|
-
// wrapper writes; it is treated as absent rather than parsed.
|
|
24
|
-
export const METERING_CHECKPOINT_LIMIT_BYTES = 64 * 1024;
|
|
25
|
-
const MAC_PATTERN = /^[0-9a-f]{64}$/u;
|
|
26
|
-
let atomicWriteSequence = 0;
|
|
27
|
-
// The checkpoint is read by a different process after this one may have died
|
|
28
|
-
// mid-write, so it is renamed into place rather than written in place: a reader
|
|
29
|
-
// sees either the previous snapshot or the next one, never a truncated file.
|
|
30
|
-
// /logs/agent is created under umask 077, which silently downgrades the
|
|
31
|
-
// writeFile mode to 0600. chmod the temporary file before rename so the
|
|
32
|
-
// published inode is already 0644; a host that races the rename must not see
|
|
33
|
-
// a 0600 file it cannot read.
|
|
34
|
-
export async function writeJsonAtomic(path, value) {
|
|
35
|
-
const temporary = `${path}.tmp-${process.pid}-${atomicWriteSequence++}`;
|
|
36
|
-
await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o644 });
|
|
37
|
-
await chmod(temporary, 0o644);
|
|
38
|
-
await rename(temporary, path);
|
|
39
|
-
}
|
|
40
|
-
// The subject can replace the checkpoint path. Refuse anything that is not a
|
|
41
|
-
// bounded regular file so a symlink or a huge write cannot be used as the
|
|
42
|
-
// host's usage evidence.
|
|
43
|
-
export async function readBoundedRegularFile(path, limitBytes = METERING_CHECKPOINT_LIMIT_BYTES) {
|
|
44
|
-
let handle;
|
|
45
|
-
try {
|
|
46
|
-
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const info = await handle.stat();
|
|
53
|
-
if (!info.isFile() || info.size <= 0 || info.size > limitBytes)
|
|
54
|
-
return undefined;
|
|
55
|
-
const bytes = Buffer.alloc(info.size);
|
|
56
|
-
const { bytesRead } = await handle.read(bytes, 0, info.size, 0);
|
|
57
|
-
if (bytesRead !== info.size)
|
|
58
|
-
return undefined;
|
|
59
|
-
return bytes;
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
await handle.close();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// The wrapper signs with the relay result token, which is host-issued and
|
|
66
|
-
// stripped from the wrapper environment before the subject starts. A subject
|
|
67
|
-
// that writes a schema-valid file cannot produce this mac.
|
|
68
|
-
export function signMeteringCheckpoint(body, secret) {
|
|
69
|
-
return { ...body, mac: createHmac('sha256', secret).update(JSON.stringify(body)).digest('hex') };
|
|
70
|
-
}
|
|
71
|
-
export function meteringCheckpointMacMatches(record, secret) {
|
|
72
|
-
const mac = record.mac;
|
|
73
|
-
if (typeof mac !== 'string' || !MAC_PATTERN.test(mac))
|
|
74
|
-
return false;
|
|
75
|
-
const body = { ...record };
|
|
76
|
-
delete body.mac;
|
|
77
|
-
const expected = Buffer.from(createHmac('sha256', secret).update(JSON.stringify(body)).digest('hex'), 'hex');
|
|
78
|
-
const actual = Buffer.from(mac, 'hex');
|
|
79
|
-
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
80
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
export function isInferenceAdmissionEvent(event, anthropic) {
|
|
20
|
-
if (anthropic && event.type === 'message_start' && isRecord(event.message))
|
|
21
|
-
return true;
|
|
22
|
-
if (typeof event.type === 'string' &&
|
|
23
|
-
(event.type === 'response.created' ||
|
|
24
|
-
event.type === 'response.in_progress' ||
|
|
25
|
-
event.type === 'response.output_item.added' ||
|
|
26
|
-
event.type === 'response.completed')) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
if (Array.isArray(event.choices))
|
|
30
|
-
return true;
|
|
31
|
-
for (const candidate of [event, event.response, event.message]) {
|
|
32
|
-
if (isRecord(candidate) && isRecord(candidate.usage))
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
function isRecord(value) {
|
|
38
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
39
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
// Cost is reported only for a complete figure. A partial token count is still
|
|
20
|
-
// worth recording as a lower bound, but a cost derived from it would enter the
|
|
21
|
-
// result kernel as a number indistinguishable from a settled one.
|
|
22
|
-
export function deriveMetering(counts) {
|
|
23
|
-
const usageComplete = counts.settled &&
|
|
24
|
-
counts.admittedRequests > 0 &&
|
|
25
|
-
counts.usageRequests === counts.admittedRequests;
|
|
26
|
-
return {
|
|
27
|
-
settledRequests: counts.requests - counts.inFlightRequests,
|
|
28
|
-
missingUsageRequests: counts.admittedRequests - counts.usageRequests,
|
|
29
|
-
usageComplete,
|
|
30
|
-
costUsd: counts.usage && usageComplete ? deepSeekCostUsd(counts.usage) : null,
|
|
31
|
-
tokenBasis: usageComplete ? 'complete' : 'lower-bound',
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
// Published DeepSeek V4 Flash prices, in USD per million tokens. Eval bills
|
|
35
|
-
// every arm from this table rather than from each arm's own transcription of
|
|
36
|
-
// it, so a difference in reported cost can only come from a difference in what
|
|
37
|
-
// the agent spent. Pi is additionally handed the table for its own display; the
|
|
38
|
-
// other frameworks are given no cost block at all, which is why their figures
|
|
39
|
-
// are not consulted here.
|
|
40
|
-
//
|
|
41
|
-
// Rates as published 2026-08-17, normalized at the off-peak band: DeepSeek
|
|
42
|
-
// bills peak hours (01:00–04:00 and 06:00–10:00 UTC) at exactly double these,
|
|
43
|
-
// and the checkpoint records no request timestamps to split by.
|
|
44
|
-
//
|
|
45
|
-
// DeepSeek bills input in exactly two kinds, cache hit and cache miss, and
|
|
46
|
-
// charges nothing for writing the cache — a written token is a miss token. So
|
|
47
|
-
// `cacheWrite` is the miss rate rather than a rate of its own: it exists to
|
|
48
|
-
// keep a provider that reports cache writes separately from being billed at
|
|
49
|
-
// zero, and for DeepSeek itself it never applies, because the API reports only
|
|
50
|
-
// `prompt_cache_hit_tokens` and `prompt_cache_miss_tokens`.
|
|
51
|
-
export const DEEPSEEK_V4_FLASH_COST = {
|
|
52
|
-
input: 0.22,
|
|
53
|
-
output: 0.66,
|
|
54
|
-
cacheRead: 0.007,
|
|
55
|
-
cacheWrite: 0.22,
|
|
56
|
-
};
|
|
57
|
-
// `inputTokens` is normalized to include both cached kinds, so each kind is
|
|
58
|
-
// subtracted out and charged at its own rate. Leaving cache writes subtracted
|
|
59
|
-
// but unpriced billed them at zero.
|
|
60
|
-
export function deepSeekCostUsd(usage) {
|
|
61
|
-
const uncached = Math.max(0, usage.inputTokens - usage.cacheReadTokens - usage.cacheWriteTokens);
|
|
62
|
-
return ((uncached * DEEPSEEK_V4_FLASH_COST.input +
|
|
63
|
-
usage.cacheReadTokens * DEEPSEEK_V4_FLASH_COST.cacheRead +
|
|
64
|
-
usage.cacheWriteTokens * DEEPSEEK_V4_FLASH_COST.cacheWrite +
|
|
65
|
-
usage.outputTokens * DEEPSEEK_V4_FLASH_COST.output) /
|
|
66
|
-
1_000_000);
|
|
67
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
const DISALLOWED_WEB_TOOL_NAMES = new Set(['websearch', 'webfetch', 'fetchurl']);
|
|
20
|
-
export function removeEvalWebTools(body) {
|
|
21
|
-
if (body.length === 0)
|
|
22
|
-
return { body, removed: 0, toolNames: [] };
|
|
23
|
-
let value;
|
|
24
|
-
try {
|
|
25
|
-
value = JSON.parse(body.toString('utf8'));
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return { body, removed: 0, toolNames: [] };
|
|
29
|
-
}
|
|
30
|
-
if (!isRecord(value))
|
|
31
|
-
return { body, removed: 0, toolNames: [] };
|
|
32
|
-
const model = typeof value.model === 'string' ? value.model : undefined;
|
|
33
|
-
if (!Array.isArray(value.tools))
|
|
34
|
-
return { body, removed: 0, toolNames: [], ...(model ? { model } : {}) };
|
|
35
|
-
const tools = value.tools.filter((tool) => !isDisallowedWebTool(tool));
|
|
36
|
-
const removed = value.tools.length - tools.length;
|
|
37
|
-
const toolNames = tools.flatMap(toolName);
|
|
38
|
-
if (removed === 0)
|
|
39
|
-
return { body, removed: 0, toolNames, ...(model ? { model } : {}) };
|
|
40
|
-
return {
|
|
41
|
-
body: Buffer.from(JSON.stringify({ ...value, tools }), 'utf8'),
|
|
42
|
-
removed,
|
|
43
|
-
toolNames,
|
|
44
|
-
...(model ? { model } : {}),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function isDisallowedWebTool(tool) {
|
|
48
|
-
if (!isRecord(tool))
|
|
49
|
-
return false;
|
|
50
|
-
const names = [tool.name, isRecord(tool.function) ? tool.function.name : undefined];
|
|
51
|
-
if (names.some((name) => typeof name === 'string' && DISALLOWED_WEB_TOOL_NAMES.has(normalize(name)))) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
if (typeof tool.type !== 'string')
|
|
55
|
-
return false;
|
|
56
|
-
const type = normalize(tool.type);
|
|
57
|
-
return [...DISALLOWED_WEB_TOOL_NAMES].some((name) => type === name || type.startsWith(name));
|
|
58
|
-
}
|
|
59
|
-
function normalize(value) {
|
|
60
|
-
return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
61
|
-
}
|
|
62
|
-
function toolName(tool) {
|
|
63
|
-
if (!isRecord(tool))
|
|
64
|
-
return [];
|
|
65
|
-
if (typeof tool.name === 'string')
|
|
66
|
-
return [tool.name];
|
|
67
|
-
if (isRecord(tool.function) && typeof tool.function.name === 'string') {
|
|
68
|
-
return [tool.function.name];
|
|
69
|
-
}
|
|
70
|
-
return typeof tool.type === 'string' ? [tool.type] : [];
|
|
71
|
-
}
|
|
72
|
-
function isRecord(value) {
|
|
73
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
74
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
import { createHash } from 'node:crypto';
|
|
20
|
-
// Keep the complete record below Linux PIPE_BUF so merged Docker stdout/stderr
|
|
21
|
-
// cannot interleave inside an otherwise valid result frame.
|
|
22
|
-
const RESULT_PAYLOAD_LIMIT_BYTES = 2 * 1024;
|
|
23
|
-
const TOKEN_PATTERN = /^[0-9a-f]{32}$/u;
|
|
24
|
-
export function takeRelayResultToken() {
|
|
25
|
-
const token = process.env.MAKA_EVAL_RESULT_TOKEN;
|
|
26
|
-
delete process.env.MAKA_EVAL_RESULT_TOKEN;
|
|
27
|
-
if (!token || !TOKEN_PATTERN.test(token))
|
|
28
|
-
throw new Error('relay result token is invalid');
|
|
29
|
-
return token;
|
|
30
|
-
}
|
|
31
|
-
export function writeRelayResult(token, value) {
|
|
32
|
-
const payload = Buffer.from(JSON.stringify(value));
|
|
33
|
-
if (payload.byteLength > RESULT_PAYLOAD_LIMIT_BYTES) {
|
|
34
|
-
throw new Error('relay result exceeds the payload limit');
|
|
35
|
-
}
|
|
36
|
-
const digest = createHash('sha256').update(payload).digest('hex');
|
|
37
|
-
process.stdout.write(`MAKA-EVAL-RESULT-V1 ${token} ${payload.byteLength} ${digest} ${payload.toString('base64url')}\n`);
|
|
38
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
import { decodeJsonObject } from './experiment.js';
|
|
20
|
-
export function decodeEvalResult(value, where = 'result') {
|
|
21
|
-
const result = exact(value, where, [
|
|
22
|
-
'score',
|
|
23
|
-
'usage',
|
|
24
|
-
'costUsd',
|
|
25
|
-
'durationMs',
|
|
26
|
-
'status',
|
|
27
|
-
'failureReason',
|
|
28
|
-
'artifacts',
|
|
29
|
-
]);
|
|
30
|
-
if (!['completed', 'subject_failed', 'infra_failed', 'indeterminate'].includes(String(result.status))) {
|
|
31
|
-
throw new Error(`${where}.status is invalid`);
|
|
32
|
-
}
|
|
33
|
-
if (result.failureReason !== null && typeof result.failureReason !== 'string') {
|
|
34
|
-
throw new Error(`${where}.failureReason is invalid`);
|
|
35
|
-
}
|
|
36
|
-
if (!Array.isArray(result.artifacts)) {
|
|
37
|
-
throw new Error(`${where}.artifacts is invalid`);
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
score: nullableNumber(result.score, `${where}.score`),
|
|
41
|
-
usage: result.usage === null ? null : decodeUsage(result.usage, `${where}.usage`),
|
|
42
|
-
costUsd: nullableNonnegative(result.costUsd, `${where}.costUsd`),
|
|
43
|
-
durationMs: nonnegative(result.durationMs, `${where}.durationMs`),
|
|
44
|
-
status: result.status,
|
|
45
|
-
failureReason: result.failureReason,
|
|
46
|
-
artifacts: result.artifacts.map((artifact, index) => decodeJsonObject(artifact, `${where}.artifacts[${index}]`)),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
export function isReplaceableAttempt(attempt) {
|
|
50
|
-
return attempt.result.status === 'infra_failed' || attempt.result.status === 'indeterminate';
|
|
51
|
-
}
|
|
52
|
-
export function selectCellResult(attempts) {
|
|
53
|
-
return [...attempts]
|
|
54
|
-
.sort((left, right) => left.sequence - right.sequence)
|
|
55
|
-
.find((attempt) => !isReplaceableAttempt(attempt));
|
|
56
|
-
}
|
|
57
|
-
function decodeUsage(value, where) {
|
|
58
|
-
const usage = exact(value, where, [
|
|
59
|
-
'inputTokens',
|
|
60
|
-
'outputTokens',
|
|
61
|
-
'cacheReadTokens',
|
|
62
|
-
'cacheWriteTokens',
|
|
63
|
-
'reasoningTokens',
|
|
64
|
-
'totalTokens',
|
|
65
|
-
]);
|
|
66
|
-
return Object.fromEntries(Object.entries(usage).map(([key, item]) => [key, nonnegative(item, `${where}.${key}`)]));
|
|
67
|
-
}
|
|
68
|
-
function exact(value, where, fields) {
|
|
69
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
70
|
-
throw new Error(`${where} must be an object`);
|
|
71
|
-
}
|
|
72
|
-
const record = value;
|
|
73
|
-
if (Object.keys(record).length !== fields.length ||
|
|
74
|
-
fields.some((field) => !Object.hasOwn(record, field))) {
|
|
75
|
-
throw new Error(`${where} fields are invalid`);
|
|
76
|
-
}
|
|
77
|
-
return record;
|
|
78
|
-
}
|
|
79
|
-
function nonnegative(value, where) {
|
|
80
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
81
|
-
throw new Error(`${where} is invalid`);
|
|
82
|
-
}
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
function nullableNumber(value, where) {
|
|
86
|
-
if (value === null)
|
|
87
|
-
return null;
|
|
88
|
-
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
89
|
-
throw new Error(`${where} is invalid`);
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
function nullableNonnegative(value, where) {
|
|
93
|
-
return value === null ? null : nonnegative(value, where);
|
|
94
|
-
}
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
-
* or more contributor license agreements. See the NOTICE file
|
|
4
|
-
* distributed with this work for additional information
|
|
5
|
-
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
-
* to you under the Apache License, Version 2.0 (the
|
|
7
|
-
* "License"); you may not use this file except in compliance
|
|
8
|
-
* with the License. You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing,
|
|
13
|
-
* software distributed under the License is distributed on an
|
|
14
|
-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
-
* KIND, either express or implied. See the License for the
|
|
16
|
-
* specific language governing permissions and limitations
|
|
17
|
-
* under the License.
|
|
18
|
-
*/
|
|
19
|
-
import { expandExperiment, } from './experiment.js';
|
|
20
|
-
import { decodeEvalResult, isReplaceableAttempt, } from './result.js';
|
|
21
|
-
export async function runExperiment(input) {
|
|
22
|
-
return input.store.runExclusive(async () => {
|
|
23
|
-
const cells = expandExperiment(input.spec);
|
|
24
|
-
const selected = selectCells(cells, input.cellIds);
|
|
25
|
-
const subjects = new Map(input.subjects.map((subject) => [subject.kind, subject]));
|
|
26
|
-
const subjectCredentialNames = [
|
|
27
|
-
...new Set(input.spec.subjects.flatMap((subject) => subject.credentials)),
|
|
28
|
-
];
|
|
29
|
-
if (input.executor.kind !== input.spec.executor.kind)
|
|
30
|
-
throw new Error('executor kind mismatch');
|
|
31
|
-
for (const cell of selected) {
|
|
32
|
-
input.executor.validate?.(cell);
|
|
33
|
-
const subject = subjects.get(cell.subject.kind);
|
|
34
|
-
if (!subject)
|
|
35
|
-
throw new Error(`missing subject adapter: ${cell.subject.kind}`);
|
|
36
|
-
subject.validate?.(cell);
|
|
37
|
-
}
|
|
38
|
-
for (const subject of subjects.values()) {
|
|
39
|
-
const cellsForSubject = selected.filter((cell) => cell.subject.kind === subject.kind);
|
|
40
|
-
if (cellsForSubject.length > 0) {
|
|
41
|
-
await subject.prepare?.({ spec: input.spec, cells: cellsForSubject });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
await runTaskGroups(groupTaskCells(selected), input.spec.execution.maxConcurrentTaskGroups, input.signal, async (group, fail) => {
|
|
45
|
-
if (input.signal?.aborted)
|
|
46
|
-
return;
|
|
47
|
-
const operations = group.map(async (cell) => {
|
|
48
|
-
if (input.signal?.aborted)
|
|
49
|
-
return;
|
|
50
|
-
const attempts = await input.store.list(cell.id);
|
|
51
|
-
if (input.signal?.aborted)
|
|
52
|
-
return;
|
|
53
|
-
const subject = subjects.get(cell.subject.kind);
|
|
54
|
-
if (selectSubjectResult(attempts, subject, cell))
|
|
55
|
-
return;
|
|
56
|
-
const startedAt = (input.now ?? Date.now)();
|
|
57
|
-
const result = await executeCell(input.executor, subject, cell, subjectCredentialNames, input.signal);
|
|
58
|
-
await input.store.append({
|
|
59
|
-
cellId: cell.id,
|
|
60
|
-
sequence: (attempts.at(-1)?.sequence ?? 0) + 1,
|
|
61
|
-
startedAt,
|
|
62
|
-
completedAt: (input.now ?? Date.now)(),
|
|
63
|
-
result,
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
await Promise.allSettled(operations.map((operation) => operation.catch((error) => {
|
|
67
|
-
fail(error);
|
|
68
|
-
throw error;
|
|
69
|
-
})));
|
|
70
|
-
});
|
|
71
|
-
return new Map((await Promise.all(cells.map(async (cell) => [
|
|
72
|
-
cell.id,
|
|
73
|
-
selectSubjectResult(await input.store.list(cell.id), subjects.get(cell.subject.kind), cell),
|
|
74
|
-
]))).flatMap(([cellId, result]) => (result ? [[cellId, result]] : [])));
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
function selectSubjectResult(attempts, subject, cell) {
|
|
78
|
-
return [...attempts]
|
|
79
|
-
.sort((left, right) => left.sequence - right.sequence)
|
|
80
|
-
.find((attempt) => !isReplaceableAttempt(attempt) && (subject.canReuse?.({ cell, attempt }) ?? true));
|
|
81
|
-
}
|
|
82
|
-
function groupTaskCells(cells) {
|
|
83
|
-
const groups = new Map();
|
|
84
|
-
for (const cell of cells) {
|
|
85
|
-
const key = `${cell.task.id}\u0000${cell.repetition}`;
|
|
86
|
-
const group = groups.get(key);
|
|
87
|
-
if (group)
|
|
88
|
-
group.push(cell);
|
|
89
|
-
else
|
|
90
|
-
groups.set(key, [cell]);
|
|
91
|
-
}
|
|
92
|
-
return [...groups.values()];
|
|
93
|
-
}
|
|
94
|
-
async function runTaskGroups(groups, maximum, signal, run) {
|
|
95
|
-
let next = 0;
|
|
96
|
-
let failed = false;
|
|
97
|
-
let failure;
|
|
98
|
-
const fail = (error) => {
|
|
99
|
-
if (failed)
|
|
100
|
-
return;
|
|
101
|
-
failed = true;
|
|
102
|
-
failure = error;
|
|
103
|
-
};
|
|
104
|
-
const worker = async () => {
|
|
105
|
-
for (;;) {
|
|
106
|
-
if (failed || signal?.aborted)
|
|
107
|
-
return;
|
|
108
|
-
const group = groups[next];
|
|
109
|
-
next += 1;
|
|
110
|
-
if (group === undefined)
|
|
111
|
-
return;
|
|
112
|
-
try {
|
|
113
|
-
await run(group, fail);
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
fail(error);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
await Promise.all(Array.from({ length: Math.min(maximum, groups.length) }, async () => await worker()));
|
|
121
|
-
if (failed)
|
|
122
|
-
throw failure;
|
|
123
|
-
}
|
|
124
|
-
async function executeCell(executor, subject, cell, subjectCredentialNames, signal) {
|
|
125
|
-
try {
|
|
126
|
-
const attempt = await executor.runAttempt({ cell, subjectCredentialNames, ...(signal ? { signal } : {}) }, async ({ context, verify }) => {
|
|
127
|
-
let execution;
|
|
128
|
-
try {
|
|
129
|
-
execution = decodeSubjectExecution(await subject.execute({
|
|
130
|
-
cell,
|
|
131
|
-
context: { ...context, ...(signal ? { signal } : {}) },
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return failure('infra_failed', 'subject execution failed');
|
|
136
|
-
}
|
|
137
|
-
if (signal?.aborted ||
|
|
138
|
-
execution.status === 'infra_failed' ||
|
|
139
|
-
execution.status === 'indeterminate') {
|
|
140
|
-
return fromUncertainSubject(execution, signal?.aborted === true);
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
const verified = decodeVerification(await verify());
|
|
144
|
-
return {
|
|
145
|
-
score: verified.score,
|
|
146
|
-
usage: execution.usage,
|
|
147
|
-
costUsd: execution.costUsd,
|
|
148
|
-
durationMs: execution.durationMs,
|
|
149
|
-
status: settledStatus(execution.status, verified.status),
|
|
150
|
-
failureReason: verified.status === 'infra_failed'
|
|
151
|
-
? verified.failureReason
|
|
152
|
-
: (execution.failureReason ?? verified.failureReason),
|
|
153
|
-
artifacts: [...execution.artifacts, ...verified.artifacts],
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return failure('infra_failed', 'verification failed', execution);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
if (attempt.kind === 'not_started') {
|
|
161
|
-
const cancelled = attempt.code === 'cancelled';
|
|
162
|
-
return failure(cancelled ? 'indeterminate' : 'infra_failed', cancelled ? 'executor preparation cancelled' : 'executor preparation failed', undefined, attempt.artifacts);
|
|
163
|
-
}
|
|
164
|
-
if (attempt.kind === 'settled')
|
|
165
|
-
return decodeEvalResult(attempt.value);
|
|
166
|
-
const failureReason = attempt.cause === 'host-cancelled'
|
|
167
|
-
? 'executor cancelled before verification completed'
|
|
168
|
-
: 'executor cleanup did not settle';
|
|
169
|
-
if (!attempt.value)
|
|
170
|
-
return failure('indeterminate', failureReason);
|
|
171
|
-
const partial = decodeEvalResult(attempt.value);
|
|
172
|
-
return {
|
|
173
|
-
...partial,
|
|
174
|
-
score: null,
|
|
175
|
-
status: 'indeterminate',
|
|
176
|
-
failureReason,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
return failure('infra_failed', 'executor preparation failed');
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function decodeSubjectExecution(value) {
|
|
184
|
-
const subject = exactRecord(value, ['usage', 'costUsd', 'durationMs', 'status', 'failureReason', 'artifacts'], ['output']);
|
|
185
|
-
if (subject.status !== 'completed' &&
|
|
186
|
-
subject.status !== 'failed' &&
|
|
187
|
-
subject.status !== 'infra_failed' &&
|
|
188
|
-
subject.status !== 'indeterminate') {
|
|
189
|
-
throw new Error('subject status is invalid');
|
|
190
|
-
}
|
|
191
|
-
if (subject.output !== undefined && typeof subject.output !== 'string') {
|
|
192
|
-
throw new Error('subject output is invalid');
|
|
193
|
-
}
|
|
194
|
-
const decoded = decodeEvalResult({
|
|
195
|
-
score: null,
|
|
196
|
-
usage: subject.usage,
|
|
197
|
-
costUsd: subject.costUsd,
|
|
198
|
-
durationMs: subject.durationMs,
|
|
199
|
-
status: subject.status === 'failed' ? 'subject_failed' : subject.status,
|
|
200
|
-
failureReason: subject.failureReason,
|
|
201
|
-
artifacts: subject.artifacts,
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
...(subject.output === undefined ? {} : { output: subject.output }),
|
|
205
|
-
usage: decoded.usage,
|
|
206
|
-
costUsd: decoded.costUsd,
|
|
207
|
-
durationMs: decoded.durationMs,
|
|
208
|
-
status: subject.status,
|
|
209
|
-
failureReason: decoded.failureReason,
|
|
210
|
-
artifacts: decoded.artifacts,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function decodeVerification(value) {
|
|
214
|
-
const verification = exactRecord(value, ['status', 'score', 'failureReason', 'artifacts']);
|
|
215
|
-
if (verification.status !== 'completed' &&
|
|
216
|
-
verification.status !== 'subject_failed' &&
|
|
217
|
-
verification.status !== 'infra_failed') {
|
|
218
|
-
throw new Error('verification status is invalid');
|
|
219
|
-
}
|
|
220
|
-
const decoded = decodeEvalResult({
|
|
221
|
-
score: verification.score,
|
|
222
|
-
usage: null,
|
|
223
|
-
costUsd: null,
|
|
224
|
-
durationMs: 0,
|
|
225
|
-
status: verification.status,
|
|
226
|
-
failureReason: verification.failureReason,
|
|
227
|
-
artifacts: verification.artifacts,
|
|
228
|
-
});
|
|
229
|
-
return {
|
|
230
|
-
status: verification.status,
|
|
231
|
-
score: decoded.score,
|
|
232
|
-
failureReason: decoded.failureReason,
|
|
233
|
-
artifacts: decoded.artifacts,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function settledStatus(subject, verification) {
|
|
237
|
-
if (verification === 'infra_failed')
|
|
238
|
-
return 'infra_failed';
|
|
239
|
-
if (subject === 'failed' || verification === 'subject_failed')
|
|
240
|
-
return 'subject_failed';
|
|
241
|
-
return 'completed';
|
|
242
|
-
}
|
|
243
|
-
function exactRecord(value, required, optional = []) {
|
|
244
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
245
|
-
throw new Error('result envelope must be an object');
|
|
246
|
-
}
|
|
247
|
-
const record = value;
|
|
248
|
-
const allowed = new Set([...required, ...optional]);
|
|
249
|
-
if (required.some((field) => !Object.hasOwn(record, field)) ||
|
|
250
|
-
Object.keys(record).some((field) => !allowed.has(field))) {
|
|
251
|
-
throw new Error('result envelope fields are invalid');
|
|
252
|
-
}
|
|
253
|
-
return record;
|
|
254
|
-
}
|
|
255
|
-
function fromUncertainSubject(subject, cancelled) {
|
|
256
|
-
return {
|
|
257
|
-
score: null,
|
|
258
|
-
usage: subject.usage,
|
|
259
|
-
costUsd: subject.costUsd,
|
|
260
|
-
durationMs: subject.durationMs,
|
|
261
|
-
status: cancelled
|
|
262
|
-
? 'indeterminate'
|
|
263
|
-
: subject.status === 'infra_failed'
|
|
264
|
-
? 'infra_failed'
|
|
265
|
-
: 'indeterminate',
|
|
266
|
-
failureReason: subject.failureReason,
|
|
267
|
-
artifacts: subject.artifacts,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
function failure(status, failureReason, subject, artifacts = subject?.artifacts ?? []) {
|
|
271
|
-
return {
|
|
272
|
-
score: null,
|
|
273
|
-
usage: subject?.usage ?? null,
|
|
274
|
-
costUsd: subject?.costUsd ?? null,
|
|
275
|
-
durationMs: subject?.durationMs ?? 0,
|
|
276
|
-
status,
|
|
277
|
-
failureReason,
|
|
278
|
-
artifacts,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function selectCells(cells, ids) {
|
|
282
|
-
if (!ids)
|
|
283
|
-
return [...cells];
|
|
284
|
-
const selected = new Set(ids);
|
|
285
|
-
const known = new Set(cells.map(({ id }) => id));
|
|
286
|
-
for (const id of selected)
|
|
287
|
-
if (!known.has(id))
|
|
288
|
-
throw new Error(`unknown experiment cell: ${id}`);
|
|
289
|
-
return cells.filter(({ id }) => selected.has(id));
|
|
290
|
-
}
|