@myagentroam/agent 0.9.163 → 0.9.165
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/agent-core/host/local-process-executor.d.ts +3 -1
- package/dist/agent-core/host/local-process-executor.js +2 -0
- package/dist/agent-core/host/local-session-environment.js +11 -0
- package/dist/agent-core/host/local-shell.d.ts +3 -0
- package/dist/agent-core/host/local-shell.js +78 -0
- package/dist/agent-core/runtime/context/gc.js +84 -72
- package/dist/agent-core/runtime/context/projection.js +3 -1
- package/dist/agent-core/session/context-gc-checkpoint.js +2 -1
- package/dist/agent-core/tools/agent/todo-read.js +1 -1
- package/dist/agent-core/tools/agent/todo-write.js +4 -7
- package/dist/agent-core/tools/agent/todo.d.ts +0 -1
- package/dist/agent-core/tools/agent/todo.js +1 -8
- package/dist/agent-core/tools/local/exec.d.ts +1 -0
- package/dist/agent-core/tools/local/exec.js +10 -1
- package/dist/sandbox/linux/bubblewrap-sandbox-executor.js +15 -2
- package/package.json +1 -1
|
@@ -4,9 +4,11 @@ export interface LocalProcessSpawnRequest {
|
|
|
4
4
|
readonly args?: readonly string[];
|
|
5
5
|
readonly cwd: string;
|
|
6
6
|
readonly shell?: boolean | string;
|
|
7
|
+
/** Shell startup for exec only; native processes and workers omit this field. */
|
|
8
|
+
readonly shellLogin?: boolean;
|
|
7
9
|
readonly environment: NodeJS.ProcessEnv;
|
|
8
10
|
/** Explicit per-start values take precedence over execution-bound Host environment. */
|
|
9
|
-
readonly environmentOverrides?: Readonly<Record<string, string>>;
|
|
11
|
+
readonly environmentOverrides?: Readonly<Record<string, string | undefined>>;
|
|
10
12
|
readonly environmentKeys?: readonly string[];
|
|
11
13
|
/** Trusted, bounded read-only worker assets; never supplied by model tool input. */
|
|
12
14
|
readonly runtimeDirectories?: readonly string[];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { prepareLocalShellProcess } from './local-shell.js';
|
|
2
3
|
/** Preserves the direct local execution behavior used by the CLI and normal Node Workbench. */
|
|
3
4
|
export const directLocalProcessExecutor = {
|
|
4
5
|
async spawn(request) {
|
|
6
|
+
request = prepareLocalShellProcess(request);
|
|
5
7
|
return spawn(request.command, [...(request.args ?? [])], {
|
|
6
8
|
cwd: request.cwd,
|
|
7
9
|
shell: request.shell ?? false,
|
|
@@ -50,12 +50,22 @@ export class LocalSessionEnvironment {
|
|
|
50
50
|
(this.platform !== 'win32' || request.environmentOverrides === undefined))
|
|
51
51
|
return request;
|
|
52
52
|
const environment = { ...request.environment };
|
|
53
|
+
const overrides = {};
|
|
53
54
|
const assign = (key, value) => {
|
|
54
55
|
if (this.platform === 'win32') {
|
|
55
56
|
for (const current of Object.keys(environment))
|
|
56
57
|
if (this.#key(current) === this.#key(key))
|
|
57
58
|
delete environment[current];
|
|
59
|
+
for (const current of Object.keys(overrides))
|
|
60
|
+
if (this.#key(current) === this.#key(key))
|
|
61
|
+
delete overrides[current];
|
|
58
62
|
}
|
|
63
|
+
Object.defineProperty(overrides, key, {
|
|
64
|
+
value,
|
|
65
|
+
enumerable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
configurable: true
|
|
68
|
+
});
|
|
59
69
|
if (value === undefined)
|
|
60
70
|
delete environment[key];
|
|
61
71
|
else
|
|
@@ -75,6 +85,7 @@ export class LocalSessionEnvironment {
|
|
|
75
85
|
return {
|
|
76
86
|
...request,
|
|
77
87
|
environment,
|
|
88
|
+
environmentOverrides: overrides,
|
|
78
89
|
...(request.environmentKeys === undefined
|
|
79
90
|
? {}
|
|
80
91
|
: {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LocalProcessSpawnRequest } from './local-process-executor.js';
|
|
2
|
+
/** Resolve Shell startup in the executor, after its environment policy has been applied. */
|
|
3
|
+
export declare function prepareLocalShellProcess(request: LocalProcessSpawnRequest, platform?: NodeJS.Platform): LocalProcessSpawnRequest;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { basename, win32 } from 'node:path';
|
|
3
|
+
/** Resolve Shell startup in the executor, after its environment policy has been applied. */
|
|
4
|
+
export function prepareLocalShellProcess(request, platform = process.platform) {
|
|
5
|
+
if (!request.shell || request.shellLogin === undefined)
|
|
6
|
+
return request;
|
|
7
|
+
const shell = typeof request.shell === 'string'
|
|
8
|
+
? request.shell
|
|
9
|
+
: platform === 'win32'
|
|
10
|
+
? (request.environment.ComSpec ?? request.environment.COMSPEC ?? 'cmd.exe')
|
|
11
|
+
: '/bin/sh';
|
|
12
|
+
const name = (platform === 'win32' ? win32.basename(shell) : basename(shell))
|
|
13
|
+
.replace(/\.exe$/iu, '')
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
const powershell = name === 'pwsh' || name === 'powershell';
|
|
16
|
+
if (!powershell && !['bash', 'zsh', 'sh'].includes(name))
|
|
17
|
+
return request;
|
|
18
|
+
const login = request.shellLogin;
|
|
19
|
+
const environment = { ...request.environment };
|
|
20
|
+
const script = [];
|
|
21
|
+
if (login && name === 'bash') {
|
|
22
|
+
script.push('if [ -z "${BASH_ENV-}" ] && [ -n "${HOME-}" ] && [ -r "$HOME/.bashrc" ]; then', ' . "$HOME/.bashrc"', 'fi');
|
|
23
|
+
}
|
|
24
|
+
else if (login && name === 'zsh') {
|
|
25
|
+
script.push('if [[ -n "${ZDOTDIR-}" ]]; then', ' [[ ! -r "$ZDOTDIR/.zshrc" ]] || . "$ZDOTDIR/.zshrc"', 'elif [[ -n "${HOME-}" ]]; then', ' [[ ! -r "$HOME/.zshrc" ]] || . "$HOME/.zshrc"', 'fi');
|
|
26
|
+
}
|
|
27
|
+
// Keep values out of argv and tool-visible command history, including quoting-sensitive values.
|
|
28
|
+
const privatePrefix = `__MAR_SHELL_${randomUUID().replaceAll('-', '')}_`;
|
|
29
|
+
const restoration = [];
|
|
30
|
+
let index = 0;
|
|
31
|
+
for (const [key, value] of Object.entries(request.environmentOverrides ?? {})) {
|
|
32
|
+
if (!powershell && !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key))
|
|
33
|
+
continue;
|
|
34
|
+
const quotedKey = `'${key.replaceAll("'", "''")}'`;
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
restoration.push(powershell
|
|
37
|
+
? `[Environment]::SetEnvironmentVariable(${quotedKey}, $null, 'Process')`
|
|
38
|
+
: `if [ "\${${key}+x}" = x ]; then unset ${key} || exit; fi`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const privateKey = `${privatePrefix}${index++}`;
|
|
42
|
+
environment[privateKey] = value;
|
|
43
|
+
restoration.push(powershell
|
|
44
|
+
? `if (-not (Test-Path Env:${privateKey})) { throw 'Shell startup removed a protected environment value.' }`
|
|
45
|
+
: `[ "\${${privateKey}+x}" = x ] || { printf '%s\\n' 'Shell startup removed a protected environment value.' >&2; exit 1; }`);
|
|
46
|
+
restoration.push(powershell
|
|
47
|
+
? `[Environment]::SetEnvironmentVariable(${quotedKey}, $env:${privateKey}, 'Process'); Remove-Item Env:${privateKey}`
|
|
48
|
+
: `if [ "\${${key}+x}" != x ] || [ "\${${key}-}" != "\$${privateKey}" ]; then export ${key}="\$${privateKey}" || exit; else export ${key} || exit; fi; unset ${privateKey}`);
|
|
49
|
+
}
|
|
50
|
+
if (restoration.length && !powershell) {
|
|
51
|
+
const traceKey = `${privatePrefix}TRACE`;
|
|
52
|
+
// A profile may enable xtrace; do not print expanded private values during restoration.
|
|
53
|
+
script.push(`case $- in *x*) set +x; ${traceKey}=1 ;; *) ${traceKey}= ;; esac`, ...restoration, `if [ "\$${traceKey}" = 1 ]; then unset ${traceKey}; set -x; else unset ${traceKey}; fi`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
script.push(...restoration);
|
|
57
|
+
}
|
|
58
|
+
script.push(request.command);
|
|
59
|
+
return {
|
|
60
|
+
...request,
|
|
61
|
+
command: shell,
|
|
62
|
+
args: [
|
|
63
|
+
...(powershell ? [...(login ? [] : ['-NoProfile']), '-Command'] : [login ? '-lc' : '-c']),
|
|
64
|
+
script.join('\n'),
|
|
65
|
+
...(request.args ?? [])
|
|
66
|
+
],
|
|
67
|
+
shell: false,
|
|
68
|
+
environment,
|
|
69
|
+
...(request.environmentKeys === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: {
|
|
72
|
+
environmentKeys: [
|
|
73
|
+
...request.environmentKeys,
|
|
74
|
+
...Object.keys(environment).filter((key) => key.startsWith(privatePrefix))
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { countModelTokens } from '../token-budget.js';
|
|
2
|
-
import { renderObservations } from '../../tools/code-mode/observations.js';
|
|
3
1
|
import { codeCallEvidence, codeObservationEvidence, retainedCodeInputs } from '../../session/context-gc-observations.js';
|
|
4
2
|
import { ContextGcCheckpointCache, contextGcBaseFromRecords, contextGcLogicalPathKey, contextGcMinimumUsageGrowth, contextGcRecordSourceHash, contextGcTargetKey, normalizeContextGcReplacements } from '../../session/context-gc-checkpoint.js';
|
|
5
3
|
import { codeCellCompletionSequence, contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcStaleReadReplacement, contextGcWebOutputReplacement, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from '../../session/context-gc-replacements.js';
|
|
@@ -30,8 +28,6 @@ const MINIMUM_CONTEXT_WINDOW_REDUCTION_RATIO = 0.05;
|
|
|
30
28
|
const COMPACT_CREDIT_ACTIVATION_RATIO = 0.8;
|
|
31
29
|
const COMPACT_THRESHOLD_RATIO = 0.9;
|
|
32
30
|
const IMMINENT_COMPACT_CREDIT_DISCOUNT = 0.8;
|
|
33
|
-
const LARGE_TERMINAL_OUTPUT_TOKENS = 4_096;
|
|
34
|
-
const HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS = 2_048;
|
|
35
31
|
export function evaluateContextGcEconomics(input) {
|
|
36
32
|
const reclaimedTokens = Math.max(0, input.reclaimedTokens);
|
|
37
33
|
const cacheReadTokens = Math.max(0, input.cacheReadTokens);
|
|
@@ -150,14 +146,15 @@ export function inspectContextGc(input) {
|
|
|
150
146
|
return {};
|
|
151
147
|
const groups = buildToolGroups(input.records, input.messages, input.messageSources);
|
|
152
148
|
const existing = previous?.payload.replacements ?? [];
|
|
149
|
+
const candidateSavings = createCandidateSavingsEstimator(input, existing);
|
|
153
150
|
const existingTargets = new Set(existing.flatMap((replacement) => [
|
|
154
151
|
contextGcTargetKey(replacement.target),
|
|
155
152
|
...(replacement.kind === 'TOOL_CALL_INPUT'
|
|
156
153
|
? replacement.providerAliasTargets.map(contextGcTargetKey)
|
|
157
154
|
: [])
|
|
158
155
|
]));
|
|
159
|
-
const stageA = collectStageACandidates(input, groups, existing, existingTargets);
|
|
160
|
-
const subagent = collectSubagentCandidates(input, groups, existingTargets);
|
|
156
|
+
const stageA = collectStageACandidates(input, groups, existing, existingTargets, candidateSavings);
|
|
157
|
+
const subagent = collectSubagentCandidates(input, groups, existingTargets, candidateSavings);
|
|
161
158
|
const deterministicCandidates = [...stageA.eligible, ...subagent.eligible];
|
|
162
159
|
const deterministicProtectedTokens = stageA.protectedTokens + subagent.protectedTokens;
|
|
163
160
|
const stageAPlan = evaluateCandidateSet(input, existing, deterministicCandidates, {
|
|
@@ -169,9 +166,9 @@ export function inspectContextGc(input) {
|
|
|
169
166
|
protectedCandidateTokens: deterministicProtectedTokens
|
|
170
167
|
});
|
|
171
168
|
if (stageAPlan?.economics.passes) {
|
|
172
|
-
const exec = collectExecCandidates(input, groups, existingTargets,
|
|
173
|
-
const codeMode = collectCodeModeCandidates(input, groups, existingTargets,
|
|
174
|
-
const web = collectWebCandidates(input, groups, existingTargets,
|
|
169
|
+
const exec = collectExecCandidates(input, groups, existingTargets, candidateSavings);
|
|
170
|
+
const codeMode = collectCodeModeCandidates(input, groups, existingTargets, candidateSavings);
|
|
171
|
+
const web = collectWebCandidates(input, groups, existingTargets, candidateSavings);
|
|
175
172
|
const protectedCandidateTokens = deterministicProtectedTokens +
|
|
176
173
|
exec.protectedTokens +
|
|
177
174
|
codeMode.protectedTokens +
|
|
@@ -202,9 +199,9 @@ export function inspectContextGc(input) {
|
|
|
202
199
|
}
|
|
203
200
|
: {};
|
|
204
201
|
}
|
|
205
|
-
const exec = collectExecCandidates(input, groups, existingTargets,
|
|
206
|
-
const codeMode = collectCodeModeCandidates(input, groups, existingTargets,
|
|
207
|
-
const web = collectWebCandidates(input, groups, existingTargets,
|
|
202
|
+
const exec = collectExecCandidates(input, groups, existingTargets, candidateSavings);
|
|
203
|
+
const codeMode = collectCodeModeCandidates(input, groups, existingTargets, candidateSavings);
|
|
204
|
+
const web = collectWebCandidates(input, groups, existingTargets, candidateSavings);
|
|
208
205
|
const combined = evaluateCandidateSet(input, existing, [...deterministicCandidates, ...exec.eligible, ...codeMode.eligible, ...web.eligible], {
|
|
209
206
|
stage: 'STAGE_A_AND_EXEC',
|
|
210
207
|
contextUsageTokens,
|
|
@@ -230,7 +227,7 @@ export function inspectContextGc(input) {
|
|
|
230
227
|
? { retryAfterRollouts }
|
|
231
228
|
: {};
|
|
232
229
|
}
|
|
233
|
-
function collectCodeModeCandidates(input, groups, existingTargets,
|
|
230
|
+
function collectCodeModeCandidates(input, groups, existingTargets, candidateSavings) {
|
|
234
231
|
const eligible = [];
|
|
235
232
|
let protectedTokens = 0;
|
|
236
233
|
let retryAfterRollouts = Infinity;
|
|
@@ -251,19 +248,8 @@ function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
251
248
|
!isRecord(completedPayload) ||
|
|
252
249
|
typeof completedPayload.truncated !== 'boolean')
|
|
253
250
|
continue;
|
|
254
|
-
const visibleTokens = contextGcMessageTokens(input, [
|
|
255
|
-
group.call.message,
|
|
256
|
-
group.result.message,
|
|
257
|
-
...group.aliases.map((alias) => alias.message)
|
|
258
|
-
]);
|
|
259
|
-
const minimumVisibleTokens = usageRatio >= COMPACT_CREDIT_ACTIVATION_RATIO
|
|
260
|
-
? HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS
|
|
261
|
-
: LARGE_TERMINAL_OUTPUT_TOKENS;
|
|
262
|
-
if (visibleTokens < minimumVisibleTokens)
|
|
263
|
-
continue;
|
|
264
251
|
for (const observation of group.result.message.observations ?? []) {
|
|
265
|
-
if (observation.type !== 'value'
|
|
266
|
-
countModelTokens(observation.value) < minimumVisibleTokens)
|
|
252
|
+
if (observation.type !== 'value')
|
|
267
253
|
continue;
|
|
268
254
|
const target = targetFor(group.result.record, {
|
|
269
255
|
...group.result.message,
|
|
@@ -285,10 +271,13 @@ function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
285
271
|
observedRollouts: successfulRolloutsAfter(input.records, Math.max(evidenceSequence, completionSequence), input.modelId, input.contextWindowTokens),
|
|
286
272
|
evidenceSequence
|
|
287
273
|
};
|
|
274
|
+
const reclaimedTokens = candidateSavings(candidate);
|
|
275
|
+
if (reclaimedTokens <= 0)
|
|
276
|
+
continue;
|
|
288
277
|
if (candidate.observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
|
|
289
278
|
eligible.push(candidate);
|
|
290
279
|
else {
|
|
291
|
-
protectedTokens +=
|
|
280
|
+
protectedTokens += reclaimedTokens;
|
|
292
281
|
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - candidate.observedRollouts);
|
|
293
282
|
}
|
|
294
283
|
}
|
|
@@ -323,10 +312,13 @@ function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
323
312
|
continue;
|
|
324
313
|
const observedRollouts = successfulRolloutsAfter(input.records, Math.max(evidenceSequence, completionSequence), input.modelId, input.contextWindowTokens);
|
|
325
314
|
const candidate = { replacements, observedRollouts, evidenceSequence };
|
|
315
|
+
const reclaimedTokens = candidateSavings(candidate);
|
|
316
|
+
if (reclaimedTokens <= 0)
|
|
317
|
+
continue;
|
|
326
318
|
if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
|
|
327
319
|
eligible.push(candidate);
|
|
328
320
|
else {
|
|
329
|
-
protectedTokens +=
|
|
321
|
+
protectedTokens += reclaimedTokens;
|
|
330
322
|
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
|
|
331
323
|
}
|
|
332
324
|
}
|
|
@@ -528,7 +520,7 @@ function evaluateCandidateSet(input, existing, candidates, state) {
|
|
|
528
520
|
eligibleCandidateCount: candidates.length
|
|
529
521
|
};
|
|
530
522
|
}
|
|
531
|
-
function collectStageACandidates(input, groups, existingReplacements, existingTargets) {
|
|
523
|
+
function collectStageACandidates(input, groups, existingReplacements, existingTargets, candidateSavings) {
|
|
532
524
|
const eligible = [];
|
|
533
525
|
let protectedTokens = 0;
|
|
534
526
|
let retryAfterRollouts = Infinity;
|
|
@@ -555,6 +547,7 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
|
|
|
555
547
|
const patchCall = patch.call;
|
|
556
548
|
const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
|
|
557
549
|
const replacements = [];
|
|
550
|
+
const candidateReadItems = new Set();
|
|
558
551
|
const patchTarget = targetFor(patchCall.record, patchCall.message);
|
|
559
552
|
if (patchTarget && !existingTargets.has(contextGcTargetKey(patchTarget)))
|
|
560
553
|
replacements.push({
|
|
@@ -578,14 +571,14 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
|
|
|
578
571
|
if (item.pathTruncated)
|
|
579
572
|
return [];
|
|
580
573
|
const itemKey = `${read.result.record.recordId}:${read.result.message.observationId ?? ''}:${item.index}`;
|
|
581
|
-
if (claimedReadItems.has(itemKey))
|
|
574
|
+
if (claimedReadItems.has(itemKey) || candidateReadItems.has(itemKey))
|
|
582
575
|
return [];
|
|
583
576
|
const pathKey = contextGcLogicalPathKey(item.path, input.workspace, input.platform);
|
|
584
577
|
const matched = result.files.find((file) => file.operation !== 'added' &&
|
|
585
578
|
contextGcLogicalPathKey(file.path, input.workspace, input.platform) === pathKey);
|
|
586
579
|
if (!pathKey || !matched || matched.operation === 'added')
|
|
587
580
|
return [];
|
|
588
|
-
|
|
581
|
+
candidateReadItems.add(itemKey);
|
|
589
582
|
return [
|
|
590
583
|
{
|
|
591
584
|
index: item.index,
|
|
@@ -625,16 +618,21 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
|
|
|
625
618
|
observedRollouts,
|
|
626
619
|
evidenceSequence
|
|
627
620
|
};
|
|
621
|
+
const reclaimedTokens = candidateSavings(candidate);
|
|
622
|
+
if (reclaimedTokens <= 0)
|
|
623
|
+
continue;
|
|
624
|
+
for (const itemKey of candidateReadItems)
|
|
625
|
+
claimedReadItems.add(itemKey);
|
|
628
626
|
if (observedRollouts >= CONTEXT_GC_POLICY.stageAProtectionRollouts)
|
|
629
627
|
eligible.push(candidate);
|
|
630
628
|
else {
|
|
631
|
-
protectedTokens +=
|
|
629
|
+
protectedTokens += reclaimedTokens;
|
|
632
630
|
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.stageAProtectionRollouts - observedRollouts);
|
|
633
631
|
}
|
|
634
632
|
}
|
|
635
633
|
return { eligible, protectedTokens, retryAfterRollouts };
|
|
636
634
|
}
|
|
637
|
-
function collectSubagentCandidates(input, groups, existingTargets) {
|
|
635
|
+
function collectSubagentCandidates(input, groups, existingTargets, candidateSavings) {
|
|
638
636
|
const eligible = [];
|
|
639
637
|
let protectedTokens = 0;
|
|
640
638
|
let retryAfterRollouts = Infinity;
|
|
@@ -714,16 +712,19 @@ function collectSubagentCandidates(input, groups, existingTargets) {
|
|
|
714
712
|
observedRollouts,
|
|
715
713
|
evidenceSequence
|
|
716
714
|
};
|
|
715
|
+
const reclaimedTokens = candidateSavings(candidate);
|
|
716
|
+
if (reclaimedTokens <= 0)
|
|
717
|
+
continue;
|
|
717
718
|
if (observedRollouts >= CONTEXT_GC_POLICY.stageAProtectionRollouts)
|
|
718
719
|
eligible.push(candidate);
|
|
719
720
|
else {
|
|
720
|
-
protectedTokens +=
|
|
721
|
+
protectedTokens += reclaimedTokens;
|
|
721
722
|
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.stageAProtectionRollouts - observedRollouts);
|
|
722
723
|
}
|
|
723
724
|
}
|
|
724
725
|
return { eligible, protectedTokens, retryAfterRollouts };
|
|
725
726
|
}
|
|
726
|
-
function collectExecCandidates(input, groups, existingTargets,
|
|
727
|
+
function collectExecCandidates(input, groups, existingTargets, candidateSavings) {
|
|
727
728
|
const eligible = [];
|
|
728
729
|
let protectedTokens = 0;
|
|
729
730
|
let retryAfterRollouts = Infinity;
|
|
@@ -797,18 +798,6 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
797
798
|
if (!terminal || (processId && input.activeProcessIds.has(processId)))
|
|
798
799
|
continue;
|
|
799
800
|
const final = lifecycle.at(-1);
|
|
800
|
-
const visibleTokens = contextGcMessageTokens(input, lifecycle
|
|
801
|
-
.filter((invocation) => !invocation.group.evidenceOnly)
|
|
802
|
-
.flatMap((invocation) => [
|
|
803
|
-
invocation.group.call.message,
|
|
804
|
-
invocation.group.result.message,
|
|
805
|
-
...invocation.group.aliases.map((alias) => alias.message)
|
|
806
|
-
]));
|
|
807
|
-
const minimumVisibleTokens = usageRatio >= COMPACT_CREDIT_ACTIVATION_RATIO
|
|
808
|
-
? HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS
|
|
809
|
-
: LARGE_TERMINAL_OUTPUT_TOKENS;
|
|
810
|
-
if (visibleTokens < minimumVisibleTokens)
|
|
811
|
-
continue;
|
|
812
801
|
const evidenceSequence = final.group.result.record.sequence;
|
|
813
802
|
const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
|
|
814
803
|
const replacements = [];
|
|
@@ -850,22 +839,22 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
850
839
|
observedRollouts,
|
|
851
840
|
evidenceSequence
|
|
852
841
|
};
|
|
842
|
+
const reclaimedTokens = candidateSavings(candidate);
|
|
843
|
+
if (reclaimedTokens <= 0)
|
|
844
|
+
continue;
|
|
853
845
|
if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
|
|
854
846
|
eligible.push(candidate);
|
|
855
847
|
else {
|
|
856
|
-
protectedTokens +=
|
|
848
|
+
protectedTokens += reclaimedTokens;
|
|
857
849
|
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
|
|
858
850
|
}
|
|
859
851
|
}
|
|
860
852
|
return { eligible, protectedTokens, retryAfterRollouts };
|
|
861
853
|
}
|
|
862
|
-
function collectWebCandidates(input, groups, existingTargets,
|
|
854
|
+
function collectWebCandidates(input, groups, existingTargets, candidateSavings) {
|
|
863
855
|
const eligible = [];
|
|
864
856
|
let protectedTokens = 0;
|
|
865
857
|
let retryAfterRollouts = Infinity;
|
|
866
|
-
const minimumVisibleTokens = usageRatio >= COMPACT_CREDIT_ACTIVATION_RATIO
|
|
867
|
-
? HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS
|
|
868
|
-
: LARGE_TERMINAL_OUTPUT_TOKENS;
|
|
869
858
|
for (const group of [...groups.values()].sort((left, right) => (left.result?.record.sequence ?? Number.MAX_SAFE_INTEGER) -
|
|
870
859
|
(right.result?.record.sequence ?? Number.MAX_SAFE_INTEGER))) {
|
|
871
860
|
if (!isContextGcWebToolName(group.toolName) ||
|
|
@@ -885,9 +874,6 @@ function collectWebCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
885
874
|
const target = targetFor(group.result.record, group.result.message);
|
|
886
875
|
if (!target || existingTargets.has(contextGcTargetKey(target)))
|
|
887
876
|
continue;
|
|
888
|
-
const visibleTokens = contextGcMessageTokens(input, [group.result.message]);
|
|
889
|
-
if (visibleTokens < minimumVisibleTokens)
|
|
890
|
-
continue;
|
|
891
877
|
const evidenceSequence = group.result.record.sequence;
|
|
892
878
|
const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
|
|
893
879
|
const candidate = {
|
|
@@ -904,10 +890,13 @@ function collectWebCandidates(input, groups, existingTargets, usageRatio) {
|
|
|
904
890
|
observedRollouts,
|
|
905
891
|
evidenceSequence
|
|
906
892
|
};
|
|
893
|
+
const reclaimedTokens = candidateSavings(candidate);
|
|
894
|
+
if (reclaimedTokens <= 0)
|
|
895
|
+
continue;
|
|
907
896
|
if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
|
|
908
897
|
eligible.push(candidate);
|
|
909
898
|
else {
|
|
910
|
-
protectedTokens +=
|
|
899
|
+
protectedTokens += reclaimedTokens;
|
|
911
900
|
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
|
|
912
901
|
}
|
|
913
902
|
}
|
|
@@ -1230,23 +1219,46 @@ function firstMatchingUsageAfter(records, sequence, modelId, contextWindowTokens
|
|
|
1230
1219
|
}
|
|
1231
1220
|
return undefined;
|
|
1232
1221
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1222
|
+
/** Measure only affected call families, retaining native aliases for provider lowering. */
|
|
1223
|
+
function createCandidateSavingsEstimator(input, existing) {
|
|
1224
|
+
const byCall = new Map();
|
|
1225
|
+
const byRecord = new Map();
|
|
1226
|
+
for (const [index, message] of input.messages.entries()) {
|
|
1227
|
+
const callId = message.role === 'provider' ? providerToolCallId(message) : message.callId;
|
|
1228
|
+
if (callId) {
|
|
1229
|
+
const indices = byCall.get(callId) ?? [];
|
|
1230
|
+
indices.push(index);
|
|
1231
|
+
byCall.set(callId, indices);
|
|
1232
|
+
}
|
|
1233
|
+
const recordId = input.messageSources[index].recordId;
|
|
1234
|
+
const indices = byRecord.get(recordId) ?? [];
|
|
1235
|
+
indices.push(index);
|
|
1236
|
+
byRecord.set(recordId, indices);
|
|
1237
|
+
}
|
|
1238
|
+
return (candidate) => {
|
|
1239
|
+
const indices = new Set();
|
|
1240
|
+
for (const replacement of candidate.replacements) {
|
|
1241
|
+
const targets = [
|
|
1242
|
+
replacement.target,
|
|
1243
|
+
...(replacement.kind === 'TOOL_CALL_INPUT' ? replacement.providerAliasTargets : [])
|
|
1244
|
+
];
|
|
1245
|
+
for (const target of targets) {
|
|
1246
|
+
for (const index of byRecord.get(target.recordId) ?? [])
|
|
1247
|
+
indices.add(index);
|
|
1248
|
+
if (target.callId)
|
|
1249
|
+
for (const index of byCall.get(target.callId) ?? [])
|
|
1250
|
+
indices.add(index);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const ordered = [...indices].sort((a, b) => a - b);
|
|
1254
|
+
const messages = ordered.map((index) => input.messages[index]);
|
|
1255
|
+
const sources = ordered.map((index) => input.messageSources[index]);
|
|
1256
|
+
const replacements = normalizeContextGcReplacements([...existing, ...candidate.replacements]);
|
|
1257
|
+
if (!replacements)
|
|
1258
|
+
return 0;
|
|
1259
|
+
const projected = applyContextGcReplacementsToMessages(messages, sources, replacements);
|
|
1260
|
+
return contextGcReclaimedTokens(input, messages, projected.messages);
|
|
1261
|
+
};
|
|
1250
1262
|
}
|
|
1251
1263
|
function targetFor(record, message) {
|
|
1252
1264
|
if (record.type !== 'model.context' ||
|
|
@@ -272,7 +272,9 @@ function targetFromMessage(record, message) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
function targetMatchesReplacement(left, right) {
|
|
275
|
-
return (contextGcTargetKey(left) === contextGcTargetKey(right) &&
|
|
275
|
+
return (contextGcTargetKey(left) === contextGcTargetKey(right) &&
|
|
276
|
+
left.sourceHash === right.sourceHash &&
|
|
277
|
+
(right.toolName === undefined || right.toolName === left.toolName));
|
|
276
278
|
}
|
|
277
279
|
function cloneMessage(message) {
|
|
278
280
|
return structuredClone(message);
|
|
@@ -58,7 +58,8 @@ export function contextGcLogicalPathKey(path, workspace, platform) {
|
|
|
58
58
|
return resolved;
|
|
59
59
|
}
|
|
60
60
|
export function contextGcTargetKey(target) {
|
|
61
|
-
|
|
61
|
+
// Tool outputs are identified by their record/call and observation, not optional tool-name metadata.
|
|
62
|
+
return `${target.sequence}:${target.recordId}:${target.role}:${target.callId ?? ''}:${target.role === 'tool' ? '' : (target.toolName ?? '')}${target.observationId ? `:${target.observationId}` : ''}`;
|
|
62
63
|
}
|
|
63
64
|
export function contextGcReplacementKey(replacement) {
|
|
64
65
|
return contextGcTargetKey(replacement.target);
|
|
@@ -2,7 +2,7 @@ export const todoReadToolDefinition = {
|
|
|
2
2
|
name: 'todo_read',
|
|
3
3
|
allowedCallers: ['direct', 'programmatic'],
|
|
4
4
|
parallelSafety: 'safe',
|
|
5
|
-
description: 'Read this session’s current Todo revision and items.
|
|
5
|
+
description: 'Read this session’s current Todo revision and items when you need the existing list. A prior read is not required to write Todo. Do not read an empty or new Todo merely to decide whether straightforward bounded work needs one.',
|
|
6
6
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false }
|
|
7
7
|
};
|
|
8
8
|
export function executeTodoRead(context) {
|
|
@@ -4,11 +4,10 @@ export const todoWriteToolDefinition = {
|
|
|
4
4
|
name: 'todo_write',
|
|
5
5
|
allowedCallers: ['direct', 'programmatic'],
|
|
6
6
|
parallelSafety: 'serial',
|
|
7
|
-
description: 'Replace this session’s entire Todo list
|
|
7
|
+
description: 'Replace this session’s entire Todo list with the supplied items; no prior read is required. Preserve stable IDs and statuses only when extending the same objective. For a materially different objective, include only the new objective’s items. Do not retain completed items from an earlier objective; when all current items are completed, default a substantive new request to replacement unless the user explicitly asks to continue, extend, verify, or follow up. Use only after Todo has been chosen for coordination-heavy work; straightforward bounded work does not need it. Keep at most one in_progress item and update statuses after meaningful stage changes. When another substantive tool call is already ready and does not depend on the Todo result, include both in the same model response instead of spending a response only for Todo bookkeeping.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
expectedRevision: { type: 'integer' },
|
|
12
11
|
items: {
|
|
13
12
|
type: 'array',
|
|
14
13
|
maxItems: TOOL_EXECUTION_LIMITS.todoMaxItems,
|
|
@@ -24,7 +23,7 @@ export const todoWriteToolDefinition = {
|
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
},
|
|
27
|
-
required: ['
|
|
26
|
+
required: ['items'],
|
|
28
27
|
additionalProperties: false
|
|
29
28
|
}
|
|
30
29
|
};
|
|
@@ -38,9 +37,7 @@ function parseTodoWriteInput(value) {
|
|
|
38
37
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
39
38
|
return invalid();
|
|
40
39
|
const input = value;
|
|
41
|
-
if (Object.keys(input).some((key) =>
|
|
42
|
-
!Number.isInteger(input.expectedRevision) ||
|
|
43
|
-
input.expectedRevision < 0 ||
|
|
40
|
+
if (Object.keys(input).some((key) => key !== 'items') ||
|
|
44
41
|
!Array.isArray(input.items) ||
|
|
45
42
|
input.items.length > TOOL_EXECUTION_LIMITS.todoMaxItems)
|
|
46
43
|
return invalid();
|
|
@@ -59,7 +56,7 @@ function parseTodoWriteInput(value) {
|
|
|
59
56
|
return invalid();
|
|
60
57
|
return { id: record.id, step: record.step, status: record.status };
|
|
61
58
|
});
|
|
62
|
-
return {
|
|
59
|
+
return { items };
|
|
63
60
|
}
|
|
64
61
|
function invalid() {
|
|
65
62
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Todo input is invalid.');
|
|
@@ -14,10 +14,6 @@ export class TodoStore {
|
|
|
14
14
|
return { revision: this.#revision, items: this.#items.map((item) => ({ ...item })) };
|
|
15
15
|
}
|
|
16
16
|
write(input) {
|
|
17
|
-
if (input.expectedRevision !== this.#revision)
|
|
18
|
-
throw new MarAgentError('MAR_AGENT_TODO_CONFLICT', 'Todo revision is stale.', {
|
|
19
|
-
details: { revision: this.#revision }
|
|
20
|
-
});
|
|
21
17
|
this.#items = normalizeTodoItems(input.items);
|
|
22
18
|
this.#revision++;
|
|
23
19
|
return this.read();
|
|
@@ -44,10 +40,7 @@ export function restoreTodoStore(snapshots) {
|
|
|
44
40
|
const items = todoItemsFromSnapshot(snapshot);
|
|
45
41
|
if (!items)
|
|
46
42
|
continue;
|
|
47
|
-
todo.write({
|
|
48
|
-
expectedRevision: todo.read().revision,
|
|
49
|
-
items
|
|
50
|
-
});
|
|
43
|
+
todo.write({ items });
|
|
51
44
|
}
|
|
52
45
|
return todo;
|
|
53
46
|
}
|
|
@@ -53,6 +53,11 @@ export const execToolDefinition = {
|
|
|
53
53
|
type: 'string',
|
|
54
54
|
description: 'Optional relative/absolute start directory; defaults to workspace. Command-internal relative paths resolve from this directory.'
|
|
55
55
|
},
|
|
56
|
+
login: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
default: true,
|
|
59
|
+
description: 'Use the Shell login/profile initialization. Defaults to true; false skips login startup. Does not force an interactive Shell.'
|
|
60
|
+
},
|
|
56
61
|
timeoutMs: {
|
|
57
62
|
type: 'integer',
|
|
58
63
|
minimum: 1,
|
|
@@ -105,7 +110,7 @@ export function execToolInput(name, value) {
|
|
|
105
110
|
return invalidExecInput();
|
|
106
111
|
const input = value;
|
|
107
112
|
const allowed = name === 'exec'
|
|
108
|
-
? ['command', 'cwd', 'timeoutMs', 'yieldMs']
|
|
113
|
+
? ['command', 'cwd', 'login', 'timeoutMs', 'yieldMs']
|
|
109
114
|
: ['processId', 'action', 'yieldMs', 'data'];
|
|
110
115
|
if (Object.keys(input).some((key) => !allowed.includes(key)))
|
|
111
116
|
return invalidExecInput();
|
|
@@ -174,6 +179,7 @@ export class ExecTool {
|
|
|
174
179
|
command: input.command,
|
|
175
180
|
cwd: cwd.physicalPath,
|
|
176
181
|
shell: this.shell ?? true,
|
|
182
|
+
shellLogin: input.login ?? true,
|
|
177
183
|
environment: this.environment,
|
|
178
184
|
displayCwd: cwd.displayPath,
|
|
179
185
|
detached: process.platform !== 'win32',
|
|
@@ -662,11 +668,14 @@ function parseExecInput(value) {
|
|
|
662
668
|
return invalidExecInput();
|
|
663
669
|
if (input.cwd !== undefined && input.cwd !== null && typeof input.cwd !== 'string')
|
|
664
670
|
return invalidExecInput();
|
|
671
|
+
if (input.login !== undefined && input.login !== null && typeof input.login !== 'boolean')
|
|
672
|
+
return invalidExecInput();
|
|
665
673
|
const timeoutMs = optionalBoundedInteger(input.timeoutMs, 1, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs);
|
|
666
674
|
return {
|
|
667
675
|
...(input.action === 'start' ? { action: 'start' } : {}),
|
|
668
676
|
command: input.command,
|
|
669
677
|
...(typeof input.cwd === 'string' ? { cwd: input.cwd } : {}),
|
|
678
|
+
...(typeof input.login === 'boolean' ? { login: input.login } : {}),
|
|
670
679
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
671
680
|
...(yieldMs === undefined ? {} : { yieldMs })
|
|
672
681
|
};
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { MarAgentError } from '../../agent-core/error.js';
|
|
8
|
+
import { prepareLocalShellProcess } from '../../agent-core/host/local-shell.js';
|
|
8
9
|
import { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION } from './profile.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const DEFAULT_BUBBLEWRAP_PATH = '/usr/bin/bwrap';
|
|
@@ -63,12 +64,24 @@ export class BubblewrapSandboxExecutor {
|
|
|
63
64
|
const mounts = await this.#mounts();
|
|
64
65
|
const runtimeMounts = await trustedRuntimeMounts(request.runtimeDirectories ?? [], mounts);
|
|
65
66
|
const cwd = await workingDirectory(request, mounts, this.options.profile.workingDirectory);
|
|
66
|
-
|
|
67
|
+
// Apply the sandbox policy before creating private startup-override variables.
|
|
68
|
+
const environment = Object.fromEntries(explicitEnvironment(request));
|
|
69
|
+
const shellRequest = prepareLocalShellProcess({
|
|
70
|
+
...request,
|
|
71
|
+
environment,
|
|
72
|
+
environmentKeys: Object.keys(environment),
|
|
73
|
+
environmentOverrides: Object.fromEntries(Object.entries(request.environmentOverrides ?? {})
|
|
74
|
+
.filter(([name, value]) => SAFE_ENVIRONMENT_NAME.test(name) &&
|
|
75
|
+
!FORBIDDEN_ENVIRONMENT_NAMES.has(name) &&
|
|
76
|
+
(value === undefined || Object.hasOwn(environment, name)))
|
|
77
|
+
.map(([name, value]) => [name, value === undefined ? undefined : environment[name]]))
|
|
78
|
+
});
|
|
79
|
+
const command = sandboxCommand(shellRequest, mounts);
|
|
67
80
|
const launchDirectory = await mkdtemp(join(tmpdir(), 'mar-agent-bwrap-launch-'));
|
|
68
81
|
const argumentPath = join(launchDirectory, 'arguments');
|
|
69
82
|
const seccompPath = join(launchDirectory, 'seccomp.bpf');
|
|
70
83
|
try {
|
|
71
|
-
const arguments_ = this.#arguments(mounts, cwd,
|
|
84
|
+
const arguments_ = this.#arguments(mounts, cwd, shellRequest, runtimeMounts);
|
|
72
85
|
await Promise.all([
|
|
73
86
|
writeFile(argumentPath, `${arguments_.join('\0')}\0`, { mode: 0o600, flag: 'wx' }),
|
|
74
87
|
writeFile(seccompPath, seccompFilter(process.arch), { mode: 0o600, flag: 'wx' })
|