@lazyingart/agintiflow 0.20.246 → 0.20.248
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/package.json +1 -1
- package/scripts/smoke-capabilities.js +28 -3
- package/scripts/smoke-context-budget-recovery.js +186 -0
- package/scripts/smoke-deep-research.js +45 -0
- package/scripts/smoke-dynamic-step-budget.js +104 -0
- package/scripts/smoke-progressive-tool-selection.js +37 -0
- package/scripts/smoke-run-stdin.js +7 -1
- package/scripts/smoke-scs-evidence-visibility.js +21 -0
- package/src/agent-runner.js +112 -21
- package/src/cli.js +8 -1
- package/src/research-routing.js +18 -0
- package/src/scs-evidence.js +22 -8
- package/src/step-budget-controller.js +20 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.248",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,6 +49,20 @@ async function runCliIn(cwd, args, envOverrides = {}) {
|
|
|
49
49
|
return result.stdout;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
async function runCliAllowStopped(args, envOverrides = {}) {
|
|
53
|
+
try {
|
|
54
|
+
const stdout = await runCli(args, envOverrides);
|
|
55
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!Number.isInteger(error?.code)) throw error;
|
|
58
|
+
return {
|
|
59
|
+
stdout: String(error.stdout || ""),
|
|
60
|
+
stderr: String(error.stderr || ""),
|
|
61
|
+
exitCode: error.code,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
try {
|
|
53
67
|
await runCli(["init"]);
|
|
54
68
|
const agintiMd = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
|
|
@@ -188,7 +202,7 @@ try {
|
|
|
188
202
|
const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
|
|
189
203
|
assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
|
|
190
204
|
assert(doctor.project.instructionsPresent, "doctor --capabilities did not report AGINTI.md");
|
|
191
|
-
const envSandboxRun = await
|
|
205
|
+
const envSandboxRun = await runCliAllowStopped(
|
|
192
206
|
["--provider", "mock", "--routing", "manual", "--model", "mock-agent", "--max-steps", "1", "env sandbox smoke"],
|
|
193
207
|
{
|
|
194
208
|
SANDBOX_MODE: "host",
|
|
@@ -196,8 +210,19 @@ try {
|
|
|
196
210
|
USE_DOCKER_SANDBOX: "false",
|
|
197
211
|
}
|
|
198
212
|
);
|
|
199
|
-
assert(envSandboxRun.
|
|
200
|
-
assert(
|
|
213
|
+
assert(envSandboxRun.exitCode === 1, "a step-budget-stopped one-shot CLI run did not report failure");
|
|
214
|
+
assert(
|
|
215
|
+
envSandboxRun.stderr.includes("Stopped after 1 steps without finish()."),
|
|
216
|
+
"a step-budget-stopped one-shot CLI run did not expose its terminal reason"
|
|
217
|
+
);
|
|
218
|
+
assert(
|
|
219
|
+
envSandboxRun.stdout.includes("Shell: host policy=allow"),
|
|
220
|
+
"one-shot CLI did not respect host sandbox env defaults"
|
|
221
|
+
);
|
|
222
|
+
assert(
|
|
223
|
+
!envSandboxRun.stdout.includes("Docker workspace:"),
|
|
224
|
+
"one-shot CLI forced Docker despite host sandbox env defaults"
|
|
225
|
+
);
|
|
201
226
|
|
|
202
227
|
console.log(
|
|
203
228
|
JSON.stringify(
|
|
@@ -51,6 +51,25 @@ const artifactContract = deriveScsTaskContract({
|
|
|
51
51
|
taskProfile: "chatops",
|
|
52
52
|
});
|
|
53
53
|
assert.equal(artifactContract.requiresExternalEvidence, true, "real chat artifact work lost its evidence gate");
|
|
54
|
+
const retainedReportContract = deriveScsTaskContract({
|
|
55
|
+
goal: [
|
|
56
|
+
"The completed evidence is already saved in tmp/reliability-evidence-pass.md and must remain read-only.",
|
|
57
|
+
"Read only missing bounded ranges of tmp/reliability-evidence-pass.md.",
|
|
58
|
+
"Rewrite agent-reliability-evidence-review.md as a concise decision document.",
|
|
59
|
+
"Rebuild sources.json so it contains only cited sources.",
|
|
60
|
+
].join("\n"),
|
|
61
|
+
taskProfile: "research",
|
|
62
|
+
});
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
retainedReportContract.exactOutputPaths,
|
|
65
|
+
["agent-reliability-evidence-review.md", "sources.json"],
|
|
66
|
+
"report continuation confused an existing saved input with rewrite/rebuild outputs"
|
|
67
|
+
);
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
retainedReportContract.exactInputPaths,
|
|
70
|
+
["tmp/reliability-evidence-pass.md"],
|
|
71
|
+
"report continuation lost the read-only evidence input"
|
|
72
|
+
);
|
|
54
73
|
assert.ok(artifactContract.requiredEvidence.some((item) => item.category === "artifact"));
|
|
55
74
|
const scopedArtifactRootContract = deriveScsTaskContract({
|
|
56
75
|
goal:
|
|
@@ -355,11 +374,176 @@ function noisyFullReadPair(index, generation) {
|
|
|
355
374
|
];
|
|
356
375
|
}
|
|
357
376
|
|
|
377
|
+
function boundedOutputReadPair(index, generation) {
|
|
378
|
+
const id = `output-${generation}-${index}`;
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
role: "assistant",
|
|
382
|
+
content: "",
|
|
383
|
+
reasoning_content: "Inspect the existing mutable output.",
|
|
384
|
+
tool_calls: [
|
|
385
|
+
{
|
|
386
|
+
id,
|
|
387
|
+
type: "function",
|
|
388
|
+
function: {
|
|
389
|
+
name: "read_file",
|
|
390
|
+
arguments: JSON.stringify({
|
|
391
|
+
path: "agent-reliability-evidence-review.md",
|
|
392
|
+
startLine: 1 + (index - 1) * 40,
|
|
393
|
+
lineLimit: 40,
|
|
394
|
+
}),
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
role: "tool",
|
|
401
|
+
tool_call_id: id,
|
|
402
|
+
content: JSON.stringify({
|
|
403
|
+
ok: true,
|
|
404
|
+
toolName: "read_file",
|
|
405
|
+
path: "agent-reliability-evidence-review.md",
|
|
406
|
+
startLine: 1 + (index - 1) * 40,
|
|
407
|
+
lineLimit: 40,
|
|
408
|
+
lineCount: 240,
|
|
409
|
+
bytes: 24000,
|
|
410
|
+
sha256: `${generation}${String(index).padStart(2, "0")}`.repeat(24).slice(0, 64),
|
|
411
|
+
contentTruncated: false,
|
|
412
|
+
content: `MUTABLE-OUTPUT-${generation}-${index}\n${"old output content ".repeat(170)}`,
|
|
413
|
+
}),
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function exactInputEvidencePair(index) {
|
|
419
|
+
const id = `exact-input-${index}`;
|
|
420
|
+
const startLine = 1 + (index - 1) * 45;
|
|
421
|
+
return [
|
|
422
|
+
{
|
|
423
|
+
role: "assistant",
|
|
424
|
+
content: "",
|
|
425
|
+
reasoning_content: "Read one bounded exact-input evidence range.",
|
|
426
|
+
tool_calls: [
|
|
427
|
+
{
|
|
428
|
+
id,
|
|
429
|
+
type: "function",
|
|
430
|
+
function: {
|
|
431
|
+
name: "read_file",
|
|
432
|
+
arguments: JSON.stringify({
|
|
433
|
+
path: "tmp/reliability-evidence-pass.md",
|
|
434
|
+
startLine,
|
|
435
|
+
lineLimit: 45,
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
role: "tool",
|
|
443
|
+
tool_call_id: id,
|
|
444
|
+
content: JSON.stringify({
|
|
445
|
+
ok: true,
|
|
446
|
+
toolName: "read_file",
|
|
447
|
+
path: "tmp/reliability-evidence-pass.md",
|
|
448
|
+
startLine,
|
|
449
|
+
lineLimit: 45,
|
|
450
|
+
lineCount: 180,
|
|
451
|
+
bytes: 24000,
|
|
452
|
+
sha256: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
|
453
|
+
contentTruncated: false,
|
|
454
|
+
content: `EXACT-INPUT-RANGE-${index}\n${`source evidence ${index} `.repeat(220)}`,
|
|
455
|
+
}),
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function readOnlyDiagnosticPair(index) {
|
|
461
|
+
const id = `diagnostic-${index}`;
|
|
462
|
+
const command = `python3 -c "print('diagnostic ${index}')"`;
|
|
463
|
+
return [
|
|
464
|
+
{
|
|
465
|
+
role: "assistant",
|
|
466
|
+
content: "",
|
|
467
|
+
reasoning_content: "Inspect one diagnostic without changing task outputs.",
|
|
468
|
+
tool_calls: [
|
|
469
|
+
{
|
|
470
|
+
id,
|
|
471
|
+
type: "function",
|
|
472
|
+
function: { name: "run_command", arguments: JSON.stringify({ command }) },
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
role: "tool",
|
|
478
|
+
tool_call_id: id,
|
|
479
|
+
content: JSON.stringify({
|
|
480
|
+
ok: true,
|
|
481
|
+
toolName: "run_command",
|
|
482
|
+
args: { command },
|
|
483
|
+
exitCode: 0,
|
|
484
|
+
stdout: `DIAGNOSTIC-${index}\n${"read-only shell output ".repeat(80)}`,
|
|
485
|
+
}),
|
|
486
|
+
},
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const exactInputCoverageState = {
|
|
491
|
+
goal: [
|
|
492
|
+
"Read tmp/reliability-evidence-pass.md as the exact read-only input.",
|
|
493
|
+
"Rewrite agent-reliability-evidence-review.md.",
|
|
494
|
+
"Rebuild sources.json.",
|
|
495
|
+
].join("\n"),
|
|
496
|
+
plan: "Use retained evidence and create the two outputs.",
|
|
497
|
+
meta: {
|
|
498
|
+
scs: {
|
|
499
|
+
taskContract: {
|
|
500
|
+
exactInputPaths: ["tmp/reliability-evidence-pass.md"],
|
|
501
|
+
exactOutputPaths: ["agent-reliability-evidence-review.md", "sources.json"],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
messages: [
|
|
506
|
+
{ role: "system", content: `SYSTEM-INPUT-COVERAGE\n${"policy ".repeat(5000)}` },
|
|
507
|
+
{ role: "user", content: "Create a source-grounded reader-facing report." },
|
|
508
|
+
...Array.from({ length: 4 }, (_, index) => exactInputEvidencePair(index + 1)).flat(),
|
|
509
|
+
...Array.from({ length: 10 }, (_, index) => readOnlyDiagnosticPair(index + 1)).flat(),
|
|
510
|
+
...Array.from({ length: 6 }, (_, index) => boundedOutputReadPair(index + 1, 9)).flat(),
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
const exactInputCoverage = buildContextBudgetCompactionMessages(
|
|
514
|
+
exactInputCoverageState,
|
|
515
|
+
{ ...config, provider: "deepseek", model: "deepseek-chat" },
|
|
516
|
+
{ title: "", url: "" },
|
|
517
|
+
20,
|
|
518
|
+
{ reason: "preserve all exact-input source ranges over diagnostics" }
|
|
519
|
+
);
|
|
520
|
+
const exactInputCoverageText = exactInputCoverage.map((message) => message.content || "").join("\n");
|
|
521
|
+
for (let index = 1; index <= 4; index += 1) {
|
|
522
|
+
assert.ok(
|
|
523
|
+
exactInputCoverageText.includes(`EXACT-INPUT-RANGE-${index}`),
|
|
524
|
+
`compaction lost exact input evidence range ${index}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
assert.ok(
|
|
528
|
+
estimateMessageTokens(exactInputCoverage) <= 12288,
|
|
529
|
+
"exact-input evidence retention exceeded the bounded retry target"
|
|
530
|
+
);
|
|
531
|
+
|
|
358
532
|
const twiceCompactedState = {
|
|
359
533
|
...compactionState,
|
|
534
|
+
meta: {
|
|
535
|
+
scs: {
|
|
536
|
+
taskContract: {
|
|
537
|
+
exactInputPaths: ["reports/reliability.md"],
|
|
538
|
+
exactOutputPaths: ["agent-reliability-evidence-review.md", "sources.json"],
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
},
|
|
360
542
|
messages: [
|
|
361
543
|
...deepSeekRuntimeMessages,
|
|
362
544
|
...Array.from({ length: 14 }, (_, index) => noisyFullReadPair(index + 1, 2)).flat(),
|
|
545
|
+
...Array.from({ length: 8 }, (_, index) => readOnlyDiagnosticPair(index + 1)).flat(),
|
|
546
|
+
...Array.from({ length: 6 }, (_, index) => boundedOutputReadPair(index + 1, 2)).flat(),
|
|
363
547
|
],
|
|
364
548
|
};
|
|
365
549
|
const twiceCompacted = buildContextBudgetCompactionMessages(
|
|
@@ -381,9 +565,11 @@ assert.equal(
|
|
|
381
565
|
|
|
382
566
|
const thriceCompactedState = {
|
|
383
567
|
...compactionState,
|
|
568
|
+
meta: twiceCompactedState.meta,
|
|
384
569
|
messages: [
|
|
385
570
|
...twiceCompacted,
|
|
386
571
|
...Array.from({ length: 14 }, (_, index) => noisyFullReadPair(index + 1, 3)).flat(),
|
|
572
|
+
...Array.from({ length: 6 }, (_, index) => boundedOutputReadPair(index + 1, 3)).flat(),
|
|
387
573
|
],
|
|
388
574
|
};
|
|
389
575
|
const thriceCompacted = buildContextBudgetCompactionMessages(
|
|
@@ -9,6 +9,7 @@ import { flushHousekeeping } from "../src/housekeeping.js";
|
|
|
9
9
|
import { requestNextStep, toolChoiceForProvider } from "../src/model-client.js";
|
|
10
10
|
import { providerStructuredOutputAttempts } from "../src/provider-contract.js";
|
|
11
11
|
import {
|
|
12
|
+
hasExplicitDeepResearchSuppression,
|
|
12
13
|
hasExplicitDeepResearchIntent,
|
|
13
14
|
hasLocalResearchWorkspaceIntent,
|
|
14
15
|
shouldStartWithDeepResearch,
|
|
@@ -107,6 +108,50 @@ async function main() {
|
|
|
107
108
|
shouldStartWithDeepResearch("Write a deep web research report comparing three primary papers."),
|
|
108
109
|
"standalone deep research no longer starts with the bounded research workflow"
|
|
109
110
|
);
|
|
111
|
+
const retainedResearchGoal = [
|
|
112
|
+
"Continue the interrupted evidence-review task from its saved state.",
|
|
113
|
+
"Do not run deep_research again. Reuse the completed research artifact.",
|
|
114
|
+
"Rewrite agent-reliability-evidence-review.md from the retained evidence.",
|
|
115
|
+
].join("\n");
|
|
116
|
+
assert(
|
|
117
|
+
hasExplicitDeepResearchSuppression(retainedResearchGoal),
|
|
118
|
+
"an explicit completed-research reuse instruction was not detected"
|
|
119
|
+
);
|
|
120
|
+
assert(
|
|
121
|
+
!shouldStartWithDeepResearch(retainedResearchGoal, inspectedLocalEvidenceMessages),
|
|
122
|
+
"an explicit prohibition still narrowed the next turn to deep_research"
|
|
123
|
+
);
|
|
124
|
+
const coordinatedSuppressionGoal = [
|
|
125
|
+
"Resume the saved evidence-review task.",
|
|
126
|
+
"Do not restart the task, run deep_research, or reopen broad discovery.",
|
|
127
|
+
"Use the retained completed evidence and rebuild sources.json.",
|
|
128
|
+
].join("\n");
|
|
129
|
+
assert(
|
|
130
|
+
hasExplicitDeepResearchSuppression(coordinatedSuppressionGoal),
|
|
131
|
+
"a coordinated do-not clause did not suppress deep_research"
|
|
132
|
+
);
|
|
133
|
+
assert(
|
|
134
|
+
!shouldStartWithDeepResearch(localEvidenceGoal, [
|
|
135
|
+
{ role: "user", content: coordinatedSuppressionGoal },
|
|
136
|
+
...inspectedLocalEvidenceMessages.slice(1),
|
|
137
|
+
]),
|
|
138
|
+
"a resumed retained-evidence repair was forced back into deep_research after inspection"
|
|
139
|
+
);
|
|
140
|
+
assert(
|
|
141
|
+
shouldStartWithDeepResearch(localEvidenceGoal, [
|
|
142
|
+
...inspectedLocalEvidenceMessages,
|
|
143
|
+
{ role: "user", content: "Run a fresh deep research pass now; the retained evidence is stale." },
|
|
144
|
+
{
|
|
145
|
+
role: "assistant",
|
|
146
|
+
tool_calls: [{ id: "fresh-report", function: { name: "read_file", arguments: '{"path":"agent-reliability-evidence-review.md"}' } }],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
role: "assistant",
|
|
150
|
+
tool_calls: [{ id: "fresh-sources", function: { name: "read_file", arguments: '{"path":"sources.json"}' } }],
|
|
151
|
+
},
|
|
152
|
+
]),
|
|
153
|
+
"an older completed-research context suppressed a newer explicit refresh request"
|
|
154
|
+
);
|
|
110
155
|
assert(
|
|
111
156
|
!hasExplicitDeepResearchIntent("Create a phone-friendly document from this folder.", [
|
|
112
157
|
{
|
|
@@ -1364,6 +1364,33 @@ try {
|
|
|
1364
1364
|
`a status wrapper changed the inner command's mutation identity: ${command}`
|
|
1365
1365
|
);
|
|
1366
1366
|
}
|
|
1367
|
+
const readOnlyValidatorState = { meta: { goalContract: { revision: 1 } } };
|
|
1368
|
+
const readOnlyValidatorResult = {
|
|
1369
|
+
toolName: "run_command",
|
|
1370
|
+
ok: true,
|
|
1371
|
+
exitCode: 0,
|
|
1372
|
+
args: {
|
|
1373
|
+
command:
|
|
1374
|
+
'python3 tmp/external_agent_reliability_quality.py . ; echo "EXIT=$?"',
|
|
1375
|
+
},
|
|
1376
|
+
stdout: "agent reliability research contract passed\nEXIT=1\n",
|
|
1377
|
+
stderr: "agent reliability research quality failed\n",
|
|
1378
|
+
commandPolicy: {
|
|
1379
|
+
category: "toolchain",
|
|
1380
|
+
writesWorkspace: true,
|
|
1381
|
+
mayMutateProject: false,
|
|
1382
|
+
substantiveTest: false,
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
recordProjectVerificationOutcome(readOnlyValidatorState, readOnlyValidatorResult, {
|
|
1386
|
+
commandCwd: workspace,
|
|
1387
|
+
taskProfile: "writing",
|
|
1388
|
+
});
|
|
1389
|
+
assert(
|
|
1390
|
+
readOnlyValidatorState.meta.projectVerification?.mutationRevision === 0 &&
|
|
1391
|
+
readOnlyValidatorResult.projectMutationRevision === 0,
|
|
1392
|
+
"a semantically read-only validator fabricated project mutation progress"
|
|
1393
|
+
);
|
|
1367
1394
|
const shellMutationState = { meta: { goalContract: { revision: 1 } } };
|
|
1368
1395
|
const shellMutationResult = {
|
|
1369
1396
|
toolName: "run_command",
|
|
@@ -2479,6 +2506,13 @@ try {
|
|
|
2479
2506
|
args: { command: "npm test && git pull --ff-only" },
|
|
2480
2507
|
stdout: "1 test passed",
|
|
2481
2508
|
stderr: "",
|
|
2509
|
+
commandPolicy: {
|
|
2510
|
+
category: "git-remote",
|
|
2511
|
+
writesWorkspace: true,
|
|
2512
|
+
mayMutateProject: false,
|
|
2513
|
+
substantiveTest: true,
|
|
2514
|
+
gitOnly: false,
|
|
2515
|
+
},
|
|
2482
2516
|
};
|
|
2483
2517
|
recordProjectVerificationOutcome(testBeforePullState, testBeforePullResult, {
|
|
2484
2518
|
commandCwd: workspace,
|
|
@@ -3067,6 +3101,21 @@ try {
|
|
|
3067
3101
|
shouldResetStaticDiscoveryPhase({ ok: true, toolName: "write_file", args: { path: "report.md" } }),
|
|
3068
3102
|
"successful output creation should reset static discovery convergence"
|
|
3069
3103
|
);
|
|
3104
|
+
assert(
|
|
3105
|
+
!shouldResetStaticDiscoveryPhase({
|
|
3106
|
+
ok: true,
|
|
3107
|
+
toolName: "run_command",
|
|
3108
|
+
args: { command: "python3 - <<'PY'\nprint('inspect only')\nPY" },
|
|
3109
|
+
commandPolicy: {
|
|
3110
|
+
writesWorkspace: true,
|
|
3111
|
+
mayMutateProject: false,
|
|
3112
|
+
substantiveTest: false,
|
|
3113
|
+
},
|
|
3114
|
+
exitCode: 0,
|
|
3115
|
+
stdout: "inspect only",
|
|
3116
|
+
}),
|
|
3117
|
+
"a read-only diagnostic shell probe reset static discovery because of a conservative write heuristic"
|
|
3118
|
+
);
|
|
3070
3119
|
const uniqueDiscovery = {};
|
|
3071
3120
|
recordStaticDiscoveryProgress(uniqueDiscovery, "read_file:/reference/A.md");
|
|
3072
3121
|
recordStaticDiscoveryProgress(uniqueDiscovery, "read_file:/reference/A.md");
|
|
@@ -3095,6 +3144,32 @@ try {
|
|
|
3095
3144
|
JSON.stringify(compactedDiscoveryState.meta.toolLoop.warned) === JSON.stringify(["run_command:keep"]),
|
|
3096
3145
|
"context recovery did not clear only stale static-read warnings"
|
|
3097
3146
|
);
|
|
3147
|
+
const retainedDiscoveryState = {
|
|
3148
|
+
meta: {
|
|
3149
|
+
toolLoop: {
|
|
3150
|
+
recent: [],
|
|
3151
|
+
warned: ["file-read:/reference/A.md"],
|
|
3152
|
+
staticCounts: { "file-read:/reference/A.md": 2 },
|
|
3153
|
+
staticOrder: ["file-read:/reference/A.md"],
|
|
3154
|
+
staticTotal: 1,
|
|
3155
|
+
staticCallTotal: 2,
|
|
3156
|
+
},
|
|
3157
|
+
},
|
|
3158
|
+
};
|
|
3159
|
+
resetStaticDiscoveryAfterContextLoss(
|
|
3160
|
+
retainedDiscoveryState,
|
|
3161
|
+
"proactive-context-compaction",
|
|
3162
|
+
{ preserveStaticEvidence: true }
|
|
3163
|
+
);
|
|
3164
|
+
assert(
|
|
3165
|
+
retainedDiscoveryState.meta.toolLoop.staticTotal === 1 &&
|
|
3166
|
+
retainedDiscoveryState.meta.toolLoop.staticCounts["file-read:/reference/A.md"] === 2,
|
|
3167
|
+
"lossless context compaction reopened already completed discovery"
|
|
3168
|
+
);
|
|
3169
|
+
assert(
|
|
3170
|
+
retainedDiscoveryState.meta.toolLoop.lastContextRecovery?.preservedStaticEvidence === true,
|
|
3171
|
+
"lossless context compaction did not record preserved discovery evidence"
|
|
3172
|
+
);
|
|
3098
3173
|
const exactReadSignature = staticToolCallSignature("read_file", { path: "/reference/A.md" }, {
|
|
3099
3174
|
commandCwd: workspace,
|
|
3100
3175
|
});
|
|
@@ -6065,6 +6140,35 @@ try {
|
|
|
6065
6140
|
events: [],
|
|
6066
6141
|
});
|
|
6067
6142
|
assert(!staticOnlyDecision.approved, "budget gate treated static discovery alone as implementation progress");
|
|
6143
|
+
const readOnlyShellDecision = decideStepBudgetExtension({
|
|
6144
|
+
config: { scsActive: true, commandCwd: "/tmp/workspace" },
|
|
6145
|
+
budget: createStepBudgetState(
|
|
6146
|
+
{ provider: "localllm", maxSteps: 12, dynamicSteps: "on", dynamicStepExtensionLimit: 2, scsActive: true },
|
|
6147
|
+
{ meta: {}, stepsCompleted: 0 }
|
|
6148
|
+
),
|
|
6149
|
+
step: 11,
|
|
6150
|
+
state: {
|
|
6151
|
+
messages: Array.from({ length: 5 }, (_, index) =>
|
|
6152
|
+
toolMessage({
|
|
6153
|
+
toolName: "run_command",
|
|
6154
|
+
ok: true,
|
|
6155
|
+
args: { command: `python3 -c "print('inspect ${index}')"` },
|
|
6156
|
+
commandPolicy: {
|
|
6157
|
+
writesWorkspace: true,
|
|
6158
|
+
mayMutateProject: false,
|
|
6159
|
+
substantiveTest: false,
|
|
6160
|
+
},
|
|
6161
|
+
exitCode: 0,
|
|
6162
|
+
stdout: `inspection ${index}`,
|
|
6163
|
+
})
|
|
6164
|
+
),
|
|
6165
|
+
},
|
|
6166
|
+
events: [],
|
|
6167
|
+
});
|
|
6168
|
+
assert(
|
|
6169
|
+
!readOnlyShellDecision.approved,
|
|
6170
|
+
"budget gate extended a run containing only read-only diagnostic shell output"
|
|
6171
|
+
);
|
|
6068
6172
|
|
|
6069
6173
|
const mockAutoBudget = createStepBudgetState({ provider: "mock", maxSteps: 4, dynamicSteps: "auto" }, { meta: {}, stepsCompleted: 0 });
|
|
6070
6174
|
assert(!mockAutoBudget.enabled, "mock provider should not auto-extend unless explicitly enabled");
|
|
@@ -1515,6 +1515,43 @@ sameNames(
|
|
|
1515
1515
|
["deep_research", "finish"],
|
|
1516
1516
|
"local evidence research did not enter the bounded research engine after inspection"
|
|
1517
1517
|
);
|
|
1518
|
+
const retainedEvidenceManifestRepair = selectProgressiveTools(allTools, {
|
|
1519
|
+
config: { provider: "deepseek" },
|
|
1520
|
+
goal:
|
|
1521
|
+
"Investigate the reliability problem in this folder, write an evidence review and sources.json, then commit the intentional work.",
|
|
1522
|
+
profile: "research",
|
|
1523
|
+
messages: [
|
|
1524
|
+
{
|
|
1525
|
+
role: "user",
|
|
1526
|
+
content: [
|
|
1527
|
+
"Resume the saved evidence-review task.",
|
|
1528
|
+
"Do not restart the task, run deep_research, or reopen broad discovery.",
|
|
1529
|
+
"Use the retained completed evidence and rebuild sources.json.",
|
|
1530
|
+
].join("\n"),
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
role: "assistant",
|
|
1534
|
+
tool_calls: [{ id: "retained-sources", function: { name: "read_file", arguments: '{"path":"sources.json"}' } }],
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
role: "assistant",
|
|
1538
|
+
tool_calls: [{ id: "retained-evidence", function: { name: "read_file", arguments: '{"path":"tmp/reliability-evidence-pass.md"}' } }],
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
});
|
|
1542
|
+
assert(
|
|
1543
|
+
names(retainedEvidenceManifestRepair).includes("write_file"),
|
|
1544
|
+
"retained-evidence manifest repair omitted write_file after inspection"
|
|
1545
|
+
);
|
|
1546
|
+
assert(
|
|
1547
|
+
names(retainedEvidenceManifestRepair).includes("read_file"),
|
|
1548
|
+
"retained-evidence manifest repair omitted bounded source reads"
|
|
1549
|
+
);
|
|
1550
|
+
assert(
|
|
1551
|
+
!(names(retainedEvidenceManifestRepair).length === 2 &&
|
|
1552
|
+
names(retainedEvidenceManifestRepair)[0] === "deep_research"),
|
|
1553
|
+
"explicit retained-evidence reuse was forced back into deep_research"
|
|
1554
|
+
);
|
|
1518
1555
|
const localEvidenceAfterDeepResearchCompaction = selectProgressiveTools(allTools, {
|
|
1519
1556
|
config: { provider: "deepseek" },
|
|
1520
1557
|
goal:
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { machineRunPayload } from "../src/cli.js";
|
|
7
|
+
import { machineRunPayload, runResultExitCode } from "../src/cli.js";
|
|
8
8
|
import { showProjectSession } from "../src/project.js";
|
|
9
9
|
|
|
10
10
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -36,6 +36,12 @@ const stoppedPayload = machineRunPayload({
|
|
|
36
36
|
if (stoppedPayload.ok !== false || stoppedPayload.failed !== true || stoppedPayload.stopped !== true) {
|
|
37
37
|
throw new Error(`stopped machine run was reported as success: ${JSON.stringify(stoppedPayload)}`);
|
|
38
38
|
}
|
|
39
|
+
if (runResultExitCode({ stopped: true, reason: "tool_contract_violation" }) !== 1) {
|
|
40
|
+
throw new Error("a stopped non-JSON agent run still reports shell success");
|
|
41
|
+
}
|
|
42
|
+
if (runResultExitCode({ stopped: false, result: "done" }) !== 0) {
|
|
43
|
+
throw new Error("a successful agent run was assigned a failing shell exit status");
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
async function runMachine(label, commandArgs, stdin = "") {
|
|
41
47
|
const command = [cliPath, ...commandArgs];
|
|
@@ -264,6 +264,27 @@ assert(
|
|
|
264
264
|
"excluding output filenames also removed a real required text term"
|
|
265
265
|
);
|
|
266
266
|
|
|
267
|
+
const wrappedManifestRepairContract = deriveScsTaskContract({
|
|
268
|
+
goal: [
|
|
269
|
+
"The only unresolved content work",
|
|
270
|
+
"is rebuilding the stale sources.json for the current report.",
|
|
271
|
+
"The verified claims are retained in",
|
|
272
|
+
"tmp/reliability-evidence-pass.md. Then write",
|
|
273
|
+
"sources.json immediately. Do not read any other file.",
|
|
274
|
+
].join("\n"),
|
|
275
|
+
taskProfile: "research",
|
|
276
|
+
});
|
|
277
|
+
assert.deepEqual(
|
|
278
|
+
wrappedManifestRepairContract.exactOutputPaths,
|
|
279
|
+
["sources.json"],
|
|
280
|
+
"an inflected, soft-wrapped output instruction did not classify its manifest as an exact output"
|
|
281
|
+
);
|
|
282
|
+
assert.deepEqual(
|
|
283
|
+
wrappedManifestRepairContract.exactInputPaths,
|
|
284
|
+
["tmp/reliability-evidence-pass.md"],
|
|
285
|
+
"a mutable exact output leaked into exact inputs through a later negated read clause"
|
|
286
|
+
);
|
|
287
|
+
|
|
267
288
|
const wordDocumentContract = deriveScsTaskContract({
|
|
268
289
|
goal: "Create an editable DOCX and a phone-friendly PDF, then verify both outputs.",
|
|
269
290
|
taskProfile: "word",
|
package/src/agent-runner.js
CHANGED
|
@@ -995,7 +995,7 @@ function compactRetainedToolPayload(toolName, payload = {}, args = {}) {
|
|
|
995
995
|
result.contentTruncatedByLines = payload.contentTruncatedByLines === true;
|
|
996
996
|
if (payload.sha256) result.sha256 = compactSingleLine(payload.sha256, 96);
|
|
997
997
|
const content = String(payload.content || payload.contentPreview || "");
|
|
998
|
-
if (content) result.content = compactMultiline(content,
|
|
998
|
+
if (content) result.content = compactMultiline(content, 3000);
|
|
999
999
|
if (Array.isArray(payload.pathEvidence)) {
|
|
1000
1000
|
result.pathEvidence = compactPathItems(payload.pathEvidence, 12, ["path", "source"]);
|
|
1001
1001
|
}
|
|
@@ -1018,7 +1018,22 @@ function compactRetainedToolPayload(toolName, payload = {}, args = {}) {
|
|
|
1018
1018
|
return redactValue(result);
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
|
-
function
|
|
1021
|
+
function retainedPathMatchesAny(sourcePath = "", candidatePaths = []) {
|
|
1022
|
+
const normalize = (value = "") =>
|
|
1023
|
+
String(value || "")
|
|
1024
|
+
.replace(/\\/g, "/")
|
|
1025
|
+
.replace(/^\.\//, "")
|
|
1026
|
+
.replace(/\/{2,}/g, "/")
|
|
1027
|
+
.replace(/\/$/, "");
|
|
1028
|
+
const source = normalize(sourcePath);
|
|
1029
|
+
if (!source) return false;
|
|
1030
|
+
return candidatePaths.some((candidate) => {
|
|
1031
|
+
const output = normalize(candidate);
|
|
1032
|
+
return output && (source === output || source.endsWith(`/${output}`) || output.endsWith(`/${source}`));
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function retainedToolRecordPriority(record = {}, outputPaths = [], inputPaths = []) {
|
|
1022
1037
|
const name = String(record.name || "");
|
|
1023
1038
|
const args = record.args || {};
|
|
1024
1039
|
const payload = record.payload || {};
|
|
@@ -1032,11 +1047,14 @@ function retainedToolRecordPriority(record = {}) {
|
|
|
1032
1047
|
} else if (name === "read_file") {
|
|
1033
1048
|
const range = retainedReadRange(payload, args);
|
|
1034
1049
|
priority = range.lineLimit > 0 ? 750 : 600;
|
|
1050
|
+
const sourcePath = String(payload.path || args.path || "").trim();
|
|
1051
|
+
if (retainedPathMatchesAny(sourcePath, inputPaths)) priority += 260;
|
|
1052
|
+
if (retainedPathMatchesAny(sourcePath, outputPaths)) priority -= 220;
|
|
1035
1053
|
} else if (["search_files", "list_files"].includes(name)) priority = 400;
|
|
1036
1054
|
return priority + Math.min(0.999, Math.max(0, Number(record.ordinal) || 0) / 100000);
|
|
1037
1055
|
}
|
|
1038
1056
|
|
|
1039
|
-
function retainedToolStateMessages(messages = [], limit = 12) {
|
|
1057
|
+
function retainedToolStateMessages(messages = [], limit = 12, outputPaths = [], inputPaths = []) {
|
|
1040
1058
|
const callsById = new Map();
|
|
1041
1059
|
const recordsByKey = new Map();
|
|
1042
1060
|
let ordinal = 0;
|
|
@@ -1086,7 +1104,8 @@ function retainedToolStateMessages(messages = [], limit = 12) {
|
|
|
1086
1104
|
const selected = [...records]
|
|
1087
1105
|
.sort(
|
|
1088
1106
|
(left, right) =>
|
|
1089
|
-
retainedToolRecordPriority(right
|
|
1107
|
+
retainedToolRecordPriority(right, outputPaths, inputPaths) -
|
|
1108
|
+
retainedToolRecordPriority(left, outputPaths, inputPaths) ||
|
|
1090
1109
|
right.ordinal - left.ordinal
|
|
1091
1110
|
)
|
|
1092
1111
|
.slice(0, Math.max(1, Number(limit) || 12))
|
|
@@ -1118,8 +1137,8 @@ function retainedToolStateMessages(messages = [], limit = 12) {
|
|
|
1118
1137
|
});
|
|
1119
1138
|
}
|
|
1120
1139
|
|
|
1121
|
-
function retainedToolStateTextMessages(messages = [], limit = 12) {
|
|
1122
|
-
const nativeMessages = retainedToolStateMessages(messages, limit);
|
|
1140
|
+
function retainedToolStateTextMessages(messages = [], limit = 12, outputPaths = [], inputPaths = []) {
|
|
1141
|
+
const nativeMessages = retainedToolStateMessages(messages, limit, outputPaths, inputPaths);
|
|
1123
1142
|
const retained = [];
|
|
1124
1143
|
for (let index = 0; index < nativeMessages.length; index += 2) {
|
|
1125
1144
|
const assistantMessage = nativeMessages[index];
|
|
@@ -1139,13 +1158,17 @@ function retainedToolStateTextMessages(messages = [], limit = 12) {
|
|
|
1139
1158
|
return retained;
|
|
1140
1159
|
}
|
|
1141
1160
|
|
|
1142
|
-
function retainedToolPairPriority(pair = [], order = 0) {
|
|
1161
|
+
function retainedToolPairPriority(pair = [], order = 0, outputPaths = [], inputPaths = []) {
|
|
1143
1162
|
const assistantCall = pair[0]?.tool_calls?.[0];
|
|
1144
1163
|
const retained = pair.length === 1 ? parseRetainedToolEvidenceMessage(pair[0]) : null;
|
|
1145
1164
|
const name = String(retained?.name || assistantCall?.function?.name || "");
|
|
1146
1165
|
const args = retained?.args || safeParseToolContent(assistantCall?.function?.arguments) || {};
|
|
1147
1166
|
const payload = retained?.payload || safeParseToolContent(pair[1]?.content) || {};
|
|
1148
|
-
return retainedToolRecordPriority(
|
|
1167
|
+
return retainedToolRecordPriority(
|
|
1168
|
+
{ name, args, payload, ordinal: order },
|
|
1169
|
+
outputPaths,
|
|
1170
|
+
inputPaths
|
|
1171
|
+
);
|
|
1149
1172
|
}
|
|
1150
1173
|
|
|
1151
1174
|
function isRuntimeCompactionRequest(content = "") {
|
|
@@ -1327,9 +1350,11 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
|
|
|
1327
1350
|
// synthetic pairs, so preserve their bounded evidence as explicit runtime
|
|
1328
1351
|
// context instead of fabricating assistant reasoning.
|
|
1329
1352
|
const deepSeekCompaction = normalizeProviderId(config.provider, "") === "deepseek";
|
|
1353
|
+
const exactOutputPaths = exactOutputPathsForState(state);
|
|
1354
|
+
const exactInputPaths = exactInputPathsForState(state);
|
|
1330
1355
|
const retainedToolMessages = deepSeekCompaction
|
|
1331
|
-
? retainedToolStateTextMessages(messages)
|
|
1332
|
-
: retainedToolStateMessages(messages);
|
|
1356
|
+
? retainedToolStateTextMessages(messages, 12, exactOutputPaths, exactInputPaths)
|
|
1357
|
+
: retainedToolStateMessages(messages, 12, exactOutputPaths, exactInputPaths);
|
|
1333
1358
|
const snapshotSummary = {
|
|
1334
1359
|
step,
|
|
1335
1360
|
maxSteps: config.maxSteps,
|
|
@@ -1407,7 +1432,7 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
|
|
|
1407
1432
|
}));
|
|
1408
1433
|
const boundedContent = compactTextForTokenBudget(
|
|
1409
1434
|
compactedContent,
|
|
1410
|
-
Math.max(1024, Math.floor(targetTokens * 0.52)),
|
|
1435
|
+
Math.max(1024, Math.floor(targetTokens * (retainedToolMessages.length ? 0.28 : 0.52))),
|
|
1411
1436
|
{ headFraction: 0.58 }
|
|
1412
1437
|
);
|
|
1413
1438
|
const baseMessages = [
|
|
@@ -1423,7 +1448,12 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
|
|
|
1423
1448
|
retainedPairs.push({
|
|
1424
1449
|
pair,
|
|
1425
1450
|
order: retainedPairs.length,
|
|
1426
|
-
priority: retainedToolPairPriority(
|
|
1451
|
+
priority: retainedToolPairPriority(
|
|
1452
|
+
pair,
|
|
1453
|
+
retainedPairs.length,
|
|
1454
|
+
exactOutputPaths,
|
|
1455
|
+
exactInputPaths
|
|
1456
|
+
),
|
|
1427
1457
|
});
|
|
1428
1458
|
}
|
|
1429
1459
|
const selectedPairs = [];
|
|
@@ -4338,7 +4368,12 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
4338
4368
|
// authorize a new invocation of the same command. A recognized trailing
|
|
4339
4369
|
// status probe reports evidence but does not change the inner command's
|
|
4340
4370
|
// mutation capability.
|
|
4341
|
-
const commandPolicy =
|
|
4371
|
+
const commandPolicy = {
|
|
4372
|
+
...classifyCommand(mutationCommand),
|
|
4373
|
+
...(toolResult.commandPolicy && typeof toolResult.commandPolicy === "object"
|
|
4374
|
+
? toolResult.commandPolicy
|
|
4375
|
+
: {}),
|
|
4376
|
+
};
|
|
4342
4377
|
const requiredCommands = effectiveRequiredProjectCommands(state, verification, config);
|
|
4343
4378
|
const requiredCommand = requiredCommands.find(
|
|
4344
4379
|
(candidate) => projectCommandsEquivalent(candidate, exitProbe.command || command, config)
|
|
@@ -5317,16 +5352,31 @@ function expectedRepeatedObservationCommand(command = "") {
|
|
|
5317
5352
|
);
|
|
5318
5353
|
}
|
|
5319
5354
|
|
|
5355
|
+
function runCommandResultHasDurableProgress(toolResult = {}) {
|
|
5356
|
+
const policy = toolResult.commandPolicy || {};
|
|
5357
|
+
const policyAllowsMutation =
|
|
5358
|
+
policy.mayMutateProject === true ||
|
|
5359
|
+
(policy.mayMutateProject === undefined && policy.writesWorkspace === true);
|
|
5360
|
+
return Boolean(
|
|
5361
|
+
policyAllowsMutation ||
|
|
5362
|
+
policy.substantiveTest === true ||
|
|
5363
|
+
(Array.isArray(toolResult.verifiedGeneratedOutputPaths) &&
|
|
5364
|
+
toolResult.verifiedGeneratedOutputPaths.length > 0)
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5320
5368
|
function isStaticDiscoveryToolResult(toolResult = {}) {
|
|
5321
5369
|
if (isStaticDiscoveryToolCall(toolResult.toolName, toolResult.args || {})) return true;
|
|
5322
5370
|
if (toolResult.toolName !== "run_command") return false;
|
|
5323
|
-
if (toolResult
|
|
5371
|
+
if (runCommandResultHasDurableProgress(toolResult)) return false;
|
|
5324
5372
|
return !expectedRepeatedObservationCommand(toolResult.args?.command);
|
|
5325
5373
|
}
|
|
5326
5374
|
|
|
5327
5375
|
function successfulToolStateProgress(toolResult = {}) {
|
|
5328
5376
|
if (!toolResult || toolResult.done || toolResult.ok === false || toolResult.blocked || toolResult.skipped) return false;
|
|
5329
|
-
if (toolResult.toolName === "run_command")
|
|
5377
|
+
if (toolResult.toolName === "run_command") {
|
|
5378
|
+
return runCommandResultHasDurableProgress(toolResult);
|
|
5379
|
+
}
|
|
5330
5380
|
if (["write_file", "apply_patch"].includes(String(toolResult.toolName || ""))) {
|
|
5331
5381
|
return successfulProjectMutationPaths(toolResult).length > 0;
|
|
5332
5382
|
}
|
|
@@ -5347,7 +5397,7 @@ function noProgressOutcomeFingerprint(toolResult = {}) {
|
|
|
5347
5397
|
toolResult?.toolName !== "run_command" ||
|
|
5348
5398
|
toolResult?.ok === false ||
|
|
5349
5399
|
toolResult?.blocked ||
|
|
5350
|
-
toolResult
|
|
5400
|
+
successfulToolStateProgress(toolResult) ||
|
|
5351
5401
|
expectedRepeatedObservationCommand(toolResult?.args?.command)
|
|
5352
5402
|
) {
|
|
5353
5403
|
return "";
|
|
@@ -6161,6 +6211,16 @@ function commandWritesOnlyPrivateVerificationEvidence(command = "") {
|
|
|
6161
6211
|
}
|
|
6162
6212
|
|
|
6163
6213
|
function commandCanMutateProjectContent(command = "", commandPolicy = {}) {
|
|
6214
|
+
// The classifier may conservatively mark an interpreter or compound shell
|
|
6215
|
+
// command as workspace-writing while still proving that this exact command
|
|
6216
|
+
// cannot mutate project content. Preserve that stronger semantic result so
|
|
6217
|
+
// validators and inspection probes do not fabricate mutation progress. Git
|
|
6218
|
+
// sequences remain structurally inspected because an aggregate Git policy
|
|
6219
|
+
// can be conservative even when one segment changes the worktree.
|
|
6220
|
+
const category = String(commandPolicy.category || "");
|
|
6221
|
+
const requiresGitMutationInspection =
|
|
6222
|
+
["git-workflow", "git-remote"].includes(category);
|
|
6223
|
+
if (commandPolicy.mayMutateProject === false && !requiresGitMutationInspection) return false;
|
|
6164
6224
|
if (commandPolicy.writesWorkspace !== true && commandPolicy.mayMutateProject !== true) return false;
|
|
6165
6225
|
const sequence = parseTopLevelShellSequence(String(command || ""));
|
|
6166
6226
|
if (
|
|
@@ -6174,7 +6234,6 @@ function commandCanMutateProjectContent(command = "", commandPolicy = {}) {
|
|
|
6174
6234
|
);
|
|
6175
6235
|
}
|
|
6176
6236
|
if (commandWritesOnlyPrivateVerificationEvidence(command)) return false;
|
|
6177
|
-
const category = String(commandPolicy.category || "");
|
|
6178
6237
|
if (!["git-workflow", "git-remote"].includes(category)) return true;
|
|
6179
6238
|
if (/\bgit\s+clone\b/i.test(String(command || ""))) return true;
|
|
6180
6239
|
// An aggregate Git category can still contain a non-Git build/generator
|
|
@@ -6682,6 +6741,18 @@ function exactOutputPathsForState(state = {}) {
|
|
|
6682
6741
|
])].slice(0, 32);
|
|
6683
6742
|
}
|
|
6684
6743
|
|
|
6744
|
+
function exactInputPathsForState(state = {}) {
|
|
6745
|
+
const scsInputPaths = Array.isArray(state.meta?.scs?.taskContract?.exactInputPaths)
|
|
6746
|
+
? state.meta.scs.taskContract.exactInputPaths.filter(Boolean)
|
|
6747
|
+
: [];
|
|
6748
|
+
const outputPaths = new Set(
|
|
6749
|
+
exactOutputPathsForState(state).map((item) => String(item).replace(/\\/g, "/").replace(/^\.\//, ""))
|
|
6750
|
+
);
|
|
6751
|
+
return [...new Set(scsInputPaths)]
|
|
6752
|
+
.filter((item) => !outputPaths.has(String(item).replace(/\\/g, "/").replace(/^\.\//, "")))
|
|
6753
|
+
.slice(0, 32);
|
|
6754
|
+
}
|
|
6755
|
+
|
|
6685
6756
|
async function hashExactOutputFile(absolutePath) {
|
|
6686
6757
|
return await new Promise((resolve, reject) => {
|
|
6687
6758
|
const digest = crypto.createHash("sha256");
|
|
@@ -7296,7 +7367,11 @@ export function recordStaticDiscoveryProgress(toolLoop = {}, signature = "") {
|
|
|
7296
7367
|
};
|
|
7297
7368
|
}
|
|
7298
7369
|
|
|
7299
|
-
export function resetStaticDiscoveryAfterContextLoss(
|
|
7370
|
+
export function resetStaticDiscoveryAfterContextLoss(
|
|
7371
|
+
state = {},
|
|
7372
|
+
reason = "context-compaction",
|
|
7373
|
+
options = {}
|
|
7374
|
+
) {
|
|
7300
7375
|
state.meta = state.meta || {};
|
|
7301
7376
|
const toolLoop = state.meta.toolLoop && typeof state.meta.toolLoop === "object"
|
|
7302
7377
|
? state.meta.toolLoop
|
|
@@ -7305,6 +7380,16 @@ export function resetStaticDiscoveryAfterContextLoss(state = {}, reason = "conte
|
|
|
7305
7380
|
const priorCounts = toolLoop.staticCounts && typeof toolLoop.staticCounts === "object"
|
|
7306
7381
|
? toolLoop.staticCounts
|
|
7307
7382
|
: {};
|
|
7383
|
+
if (options.preserveStaticEvidence === true) {
|
|
7384
|
+
toolLoop.lastContextRecovery = {
|
|
7385
|
+
reason: String(reason || "context-compaction"),
|
|
7386
|
+
at: new Date().toISOString(),
|
|
7387
|
+
priorStaticTotal: Number(priorOrder.length),
|
|
7388
|
+
preservedStaticEvidence: true,
|
|
7389
|
+
};
|
|
7390
|
+
state.meta.toolLoop = toolLoop;
|
|
7391
|
+
return toolLoop.lastContextRecovery;
|
|
7392
|
+
}
|
|
7308
7393
|
if (priorOrder.length || Object.keys(priorCounts).length) {
|
|
7309
7394
|
const history = Array.isArray(toolLoop.staticHistory) ? toolLoop.staticHistory : [];
|
|
7310
7395
|
history.push({
|
|
@@ -11025,7 +11110,9 @@ export async function runAgent(config) {
|
|
|
11025
11110
|
const tokensAfter = estimateMessageTokens(compactMessages);
|
|
11026
11111
|
if (charsAfter < contextDecision.charsBefore) {
|
|
11027
11112
|
state.messages = compactMessages;
|
|
11028
|
-
resetStaticDiscoveryAfterContextLoss(state, "proactive-context-compaction"
|
|
11113
|
+
resetStaticDiscoveryAfterContextLoss(state, "proactive-context-compaction", {
|
|
11114
|
+
preserveStaticEvidence: true,
|
|
11115
|
+
});
|
|
11029
11116
|
state.meta.contextBudget = recordContextCompaction(contextBudget, {
|
|
11030
11117
|
step,
|
|
11031
11118
|
charsBefore: contextDecision.charsBefore,
|
|
@@ -11141,7 +11228,9 @@ export async function runAgent(config) {
|
|
|
11141
11228
|
};
|
|
11142
11229
|
state.messages = compactMessages;
|
|
11143
11230
|
requestMessages = compactMessages;
|
|
11144
|
-
resetStaticDiscoveryAfterContextLoss(state, "local-context-budget-retry"
|
|
11231
|
+
resetStaticDiscoveryAfterContextLoss(state, "local-context-budget-retry", {
|
|
11232
|
+
preserveStaticEvidence: true,
|
|
11233
|
+
});
|
|
11145
11234
|
state.meta.localContextBudgetRetries = {
|
|
11146
11235
|
...contextRetriedSteps,
|
|
11147
11236
|
[retryKey]: true,
|
|
@@ -11189,7 +11278,9 @@ export async function runAgent(config) {
|
|
|
11189
11278
|
};
|
|
11190
11279
|
state.messages = compactMessages;
|
|
11191
11280
|
requestMessages = compactMessages;
|
|
11192
|
-
resetStaticDiscoveryAfterContextLoss(state, "model-timeout-retry"
|
|
11281
|
+
resetStaticDiscoveryAfterContextLoss(state, "model-timeout-retry", {
|
|
11282
|
+
preserveStaticEvidence: true,
|
|
11283
|
+
});
|
|
11193
11284
|
state.meta.modelTimeoutRetries = {
|
|
11194
11285
|
...retriedSteps,
|
|
11195
11286
|
[retryKey]: true,
|
package/src/cli.js
CHANGED
|
@@ -383,6 +383,10 @@ export function machineRunPayload(run, fallbackSessionId = "", metadata = {}) {
|
|
|
383
383
|
};
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
export function runResultExitCode(run = {}) {
|
|
387
|
+
return run?.stopped === true || run?.failed === true ? 1 : 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
386
390
|
function printMachineRunResult(run, fallbackSessionId = "", metadata = {}) {
|
|
387
391
|
const payload = machineRunPayload(run, fallbackSessionId, metadata);
|
|
388
392
|
console.log(JSON.stringify(payload));
|
|
@@ -2507,6 +2511,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2507
2511
|
...(jsonFlag ? { onConsole: () => {} } : {}),
|
|
2508
2512
|
});
|
|
2509
2513
|
const run = await runAgent(config);
|
|
2514
|
+
if (runResultExitCode(run) !== 0) process.exitCode = 1;
|
|
2510
2515
|
if (jsonFlag) {
|
|
2511
2516
|
printMachineRunResult(run, runArgs.sessionId, {
|
|
2512
2517
|
provider: config.provider,
|
|
@@ -2604,6 +2609,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2604
2609
|
config.expectedRuntimeRevision = preparedRuntime.expectedRuntimeRevision;
|
|
2605
2610
|
if (preparedRuntime.runtimePatch) config.runtimePatch = preparedRuntime.runtimePatch;
|
|
2606
2611
|
const run = await runAgent(config);
|
|
2612
|
+
if (runResultExitCode(run) !== 0) process.exitCode = 1;
|
|
2607
2613
|
if (jsonFlag) {
|
|
2608
2614
|
printMachineRunResult(run, sessionId, {
|
|
2609
2615
|
provider: config.provider,
|
|
@@ -2765,5 +2771,6 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2765
2771
|
config.expectedRuntimeRevision = preparedRuntime.expectedRuntimeRevision;
|
|
2766
2772
|
if (preparedRuntime.runtimePatch) config.runtimePatch = preparedRuntime.runtimePatch;
|
|
2767
2773
|
}
|
|
2768
|
-
await runAgent(config);
|
|
2774
|
+
const run = await runAgent(config);
|
|
2775
|
+
if (runResultExitCode(run) !== 0) process.exitCode = 1;
|
|
2769
2776
|
}
|
package/src/research-routing.js
CHANGED
|
@@ -95,12 +95,30 @@ export function hasLocalResearchWorkspaceIntent(goal = "", messages = []) {
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export function hasExplicitDeepResearchSuppression(goal = "", messages = []) {
|
|
99
|
+
const current = currentIntentMessages(messages);
|
|
100
|
+
const latestUserIntent = [...current]
|
|
101
|
+
.reverse()
|
|
102
|
+
.find((message) => message?.role === "user" && !isRuntimeUserMessage(messageText(message.content)));
|
|
103
|
+
const text = `${scopedChatopsEvidenceGoal(goal)}\n${scopedChatopsEvidenceGoal(
|
|
104
|
+
messageText(latestUserIntent?.content)
|
|
105
|
+
)}`;
|
|
106
|
+
return (
|
|
107
|
+
/\b(?:do not|don't|must not|never)\s+(?:run|rerun|re-run|repeat|restart|invoke|call|start)\s+(?:the\s+)?(?:deep[_ -]?research|research workflow)\b/i.test(text) ||
|
|
108
|
+
/\b(?:do not|don't|must not|never)\b[^.!?\n]{0,140}\b(?:run|rerun|re-run|repeat|restart|invoke|call|start)\s+(?:the\s+)?(?:deep[_ -]?research|research workflow)\b/i.test(text) ||
|
|
109
|
+
/\b(?:reuse|use|continue from|recover from)\b.{0,140}\b(?:completed|existing|retained|saved)\b.{0,180}\b(?:deep[_ -]?research|research (?:result|artifact|evidence|pass))\b/i.test(text) ||
|
|
110
|
+
/(?:不要|无需|不必|禁止).{0,20}(?:重新|再次|重复)?(?:运行|调用|启动)?(?:深度研究|深入研究|deep[_ -]?research)/iu.test(text) ||
|
|
111
|
+
/(?:ディープリサーチ|深い調査).{0,20}(?:再実行しない|繰り返さない|呼び出さない)/u.test(text)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
export function toolWasRequested(messages = [], toolName = "") {
|
|
99
116
|
return requestedToolCount(messages, toolName) > 0;
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
export function shouldStartWithDeepResearch(goal = "", messages = []) {
|
|
103
120
|
if (!hasExplicitDeepResearchIntent(goal, messages)) return false;
|
|
121
|
+
if (hasExplicitDeepResearchSuppression(goal, messages)) return false;
|
|
104
122
|
const current = currentIntentMessages(messages);
|
|
105
123
|
if (toolWasRequested(current, "deep_research")) return false;
|
|
106
124
|
if (hasLocalResearchWorkspaceIntent(goal, messages) && !localWorkspaceInspectionReady(current)) return false;
|
package/src/scs-evidence.js
CHANGED
|
@@ -373,9 +373,9 @@ function inferExactOutputPaths(goal = "") {
|
|
|
373
373
|
"gi"
|
|
374
374
|
);
|
|
375
375
|
const directOutputAction =
|
|
376
|
-
/\b(
|
|
376
|
+
/\b(?:sav(?:e|es|ing)|writ(?:e|es|ing)|rewrit(?:e|es|ing)|output(?:s|ting)?|creat(?:e|es|ing)|rebuild(?:s|ing)?|replac(?:e|es|ing)|regenerat(?:e|es|ing)|generat(?:e|es|ing)|stor(?:e|es|ing)|updat(?:e|es|ing)|modif(?:y|ies|ying)|edit(?:s|ing)?)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/i;
|
|
377
377
|
const directOutputActionGlobal =
|
|
378
|
-
/\b(
|
|
378
|
+
/\b(?:sav(?:e|es|ing)|writ(?:e|es|ing)|rewrit(?:e|es|ing)|output(?:s|ting)?|creat(?:e|es|ing)|rebuild(?:s|ing)?|replac(?:e|es|ing)|regenerat(?:e|es|ing)|generat(?:e|es|ing)|stor(?:e|es|ing)|updat(?:e|es|ing)|modif(?:y|ies|ying)|edit(?:s|ing)?)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/gi;
|
|
379
379
|
const outputListHeader =
|
|
380
380
|
/^(?:#+\s*)?(?:(?:required|final|expected|declared|target|pilot|deliverable)\s+)*(?:create|created files?|files? to create|outputs?|output structure|required outputs?|artifacts?|deliverables?|generated files?|writer requirements|renderer requirements|生成文件|输出结构|輸出結構|输出文件|輸出文件|创建文件|建立文件)(?:\s+(?:outputs?|artifacts?|deliverables?))?\s*[::]?\s*$/i;
|
|
381
381
|
const nonOutputToolLine =
|
|
@@ -410,8 +410,15 @@ function inferExactOutputPaths(goal = "") {
|
|
|
410
410
|
}
|
|
411
411
|
return matches[0]?.index ?? -1;
|
|
412
412
|
};
|
|
413
|
-
for (
|
|
414
|
-
const
|
|
413
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
414
|
+
const rawLine = lines[lineIndex];
|
|
415
|
+
const currentLine = String(rawLine || "").trim();
|
|
416
|
+
const previousLine = String(lines[lineIndex - 1] || "").trim();
|
|
417
|
+
const wrappedOutputInstruction =
|
|
418
|
+
previousLine &&
|
|
419
|
+
!/[.!?。!?;;]$/.test(previousLine) &&
|
|
420
|
+
directOutputAction.test(previousLine);
|
|
421
|
+
const line = wrappedOutputInstruction ? `${previousLine} ${currentLine}`.trim() : currentLine;
|
|
415
422
|
if (!line) {
|
|
416
423
|
if (inOutputList) inOutputList = false;
|
|
417
424
|
continue;
|
|
@@ -468,16 +475,23 @@ function inferExactInputPaths(goal = "") {
|
|
|
468
475
|
"gi"
|
|
469
476
|
);
|
|
470
477
|
const inputAction =
|
|
471
|
-
/\b(use|using|read|load|fill|upload|attach|import|select|choose|reference|input|from|fix|repair|patch|correct)\b
|
|
478
|
+
/\b(?:use|using|read|load|fill|upload|attach|import|select|choose|reference|input|from|retain(?:ed|ing)?|fix|repair|patch|correct)\b|使用|读取|讀取|加载|載入|填写|填入|上传|上傳|附加|导入|導入|选择|選擇|选取|選取|参考|參考|素材|图片|圖片|照片|提示词|提示詞|保留|修复|修正|更正|从|從/i;
|
|
472
479
|
const directOutputAction =
|
|
473
|
-
/\b(
|
|
480
|
+
/\b(?:sav(?:e|es|ing)|writ(?:e|es|ing)|rewrit(?:e|es|ing)|output(?:s|ting)?|creat(?:e|es|ing)|rebuild(?:s|ing)?|replac(?:e|es|ing)|regenerat(?:e|es|ing)|generat(?:e|es|ing)|stor(?:e|es|ing)|updat(?:e|es|ing)|modif(?:y|ies|ying)|edit(?:s|ing)?)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/i;
|
|
474
481
|
const pushPath = (raw = "") => {
|
|
475
482
|
const cleaned = String(raw || "").trim();
|
|
476
483
|
if (!cleaned || /[{}]/.test(cleaned)) return;
|
|
477
484
|
paths.push(cleaned);
|
|
478
485
|
};
|
|
479
|
-
for (
|
|
480
|
-
const
|
|
486
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
487
|
+
const rawLine = lines[lineIndex];
|
|
488
|
+
const currentLine = String(rawLine || "").trim();
|
|
489
|
+
const previousLine = String(lines[lineIndex - 1] || "").trim();
|
|
490
|
+
const wrappedInputInstruction =
|
|
491
|
+
previousLine &&
|
|
492
|
+
!/[.!?。!?;;]$/.test(previousLine) &&
|
|
493
|
+
inputAction.test(previousLine);
|
|
494
|
+
const fullLine = wrappedInputInstruction ? `${previousLine} ${currentLine}`.trim() : currentLine;
|
|
481
495
|
const outputIndex = fullLine.search(directOutputAction);
|
|
482
496
|
const line = outputIndex > 0 ? fullLine.slice(0, outputIndex).trim() : fullLine;
|
|
483
497
|
if (!line || !inputAction.test(line)) continue;
|
|
@@ -208,7 +208,25 @@ export function staticToolCallSignature(toolName, args = {}, context = {}) {
|
|
|
208
208
|
|
|
209
209
|
function isStaticDiscoveryResult(result = {}) {
|
|
210
210
|
if (!result || result.ok === false || result.blocked || result.done) return false;
|
|
211
|
-
|
|
211
|
+
if (isStaticDiscoveryToolCall(result.toolName, result.args || {})) return true;
|
|
212
|
+
if (result.toolName !== "run_command") return false;
|
|
213
|
+
if (/\b(?:watch|poll|status|queue|sleep)\b|tail\s+-f|tmux\s+capture-pane/i.test(String(result.args?.command || ""))) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return !runCommandHasConcreteProgress(result);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function runCommandHasConcreteProgress(result = {}) {
|
|
220
|
+
const policy = result.commandPolicy || {};
|
|
221
|
+
const policyAllowsMutation =
|
|
222
|
+
policy.mayMutateProject === true ||
|
|
223
|
+
(policy.mayMutateProject === undefined && policy.writesWorkspace === true);
|
|
224
|
+
return Boolean(
|
|
225
|
+
policyAllowsMutation ||
|
|
226
|
+
policy.substantiveTest === true ||
|
|
227
|
+
(Array.isArray(result.verifiedGeneratedOutputPaths) &&
|
|
228
|
+
result.verifiedGeneratedOutputPaths.length > 0)
|
|
229
|
+
);
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
export function summarizeRepeatedStaticDiscovery(recentToolResults = [], context = {}) {
|
|
@@ -251,7 +269,7 @@ function hasConcreteProgress(recentToolResults = [], events = []) {
|
|
|
251
269
|
if (result.ok === false || result.blocked || result.done) return false;
|
|
252
270
|
if (isStaticDiscoveryToolCall(result.toolName, result.args || {})) return false;
|
|
253
271
|
if (!PROGRESS_TOOL_NAMES.has(result.toolName)) return false;
|
|
254
|
-
if (result.toolName === "run_command") return
|
|
272
|
+
if (result.toolName === "run_command") return runCommandHasConcreteProgress(result);
|
|
255
273
|
return Boolean(
|
|
256
274
|
result.path ||
|
|
257
275
|
result.artifactPath ||
|