ucode-agent 1.39.0 → 1.40.0
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/src/core/loop.js +69 -3
package/package.json
CHANGED
package/src/core/loop.js
CHANGED
|
@@ -103,6 +103,9 @@ const SILENT = new Set(['update_plan']);
|
|
|
103
103
|
const MAX_FIX_ROUNDS = 3;
|
|
104
104
|
|
|
105
105
|
/** Files worth checking after they change. */
|
|
106
|
+
/** A file with a page in it — something a browser can be pointed at. */
|
|
107
|
+
const PAGE = /\.(?:html?|tsx|jsx)$/i;
|
|
108
|
+
|
|
106
109
|
const CHECKABLE = /\.(?:[cm]?[jt]sx?|py|html?)$/i;
|
|
107
110
|
|
|
108
111
|
/** Where TypeScript keeps what it learned, so the next check is a quick one. */
|
|
@@ -1082,6 +1085,8 @@ export class Agent {
|
|
|
1082
1085
|
// /undo can put the whole turn back.
|
|
1083
1086
|
beginTurn();
|
|
1084
1087
|
this.lookedThisTurn = false;
|
|
1088
|
+
this.reads = new Map();
|
|
1089
|
+
this.declines = 0;
|
|
1085
1090
|
forgetReviews(); // a new request: its apps get a fresh design review
|
|
1086
1091
|
const images = await this.attachImages(input);
|
|
1087
1092
|
this.push(images.length
|
|
@@ -1641,7 +1646,14 @@ export class Agent {
|
|
|
1641
1646
|
// changed so far; the automatic check at the end would only repeat it.
|
|
1642
1647
|
if (call.name === 'run_command' && out.exitCode === 0 &&
|
|
1643
1648
|
/(?:next build|npm run build|pnpm (?:run )?build|tsc)/.test(call.args?.command ?? '')) {
|
|
1649
|
+
// The pages stay. A build proves the project compiles, which is not the
|
|
1650
|
+
// same claim as the page working — and clearing everything here meant a
|
|
1651
|
+
// Next.js app, where a passing `npm run build` is part of every turn,
|
|
1652
|
+
// never reached the look at all. That is the one check that opens the
|
|
1653
|
+
// thing and presses its buttons, skipped on the whole framework.
|
|
1654
|
+
const pages = [...(this.sinceCheck ?? [])].filter((f) => PAGE.test(f));
|
|
1644
1655
|
this.sinceCheck?.clear();
|
|
1656
|
+
for (const p of pages) this.sinceCheck?.add(p);
|
|
1645
1657
|
}
|
|
1646
1658
|
if (!QUIET.has(call.name)) this.ui.toolResult(out.summary);
|
|
1647
1659
|
// The change as its two numbers, not as a copy of the file. The diff rows
|
|
@@ -1670,11 +1682,32 @@ export class Agent {
|
|
|
1670
1682
|
// user made, and anything that actually failed, still shows.
|
|
1671
1683
|
if (err instanceof Declined) this.ui.toolFailed('declined');
|
|
1672
1684
|
else if (err.kind !== 'bad_args') this.ui.toolFailed(`${err.kind}: ${err.failed}`);
|
|
1685
|
+
|
|
1686
|
+
// Declines that keep coming are not decisions, they are a wall.
|
|
1687
|
+
//
|
|
1688
|
+
// A traced build ran 250 steps and 32 minutes before dying on the step
|
|
1689
|
+
// limit because every command it tried was declined — a piped session
|
|
1690
|
+
// cannot answer "go ahead? [y/N]", so the answer is always no. The model
|
|
1691
|
+
// read each refusal as being about that command, picked a different one,
|
|
1692
|
+
// and was refused again. Nothing ever told it the refusals were the room
|
|
1693
|
+
// rather than the request.
|
|
1694
|
+
//
|
|
1695
|
+
// Two are a person saying no to two things. The third is the wall, and it
|
|
1696
|
+
// is worth naming, because the way out is to finish without commands
|
|
1697
|
+
// rather than to keep hunting for one that is allowed.
|
|
1698
|
+
this.declines = err instanceof Declined ? (this.declines ?? 0) + 1 : 0;
|
|
1673
1699
|
this.push({
|
|
1674
1700
|
role: 'tool',
|
|
1675
1701
|
toolCallId: call.id,
|
|
1676
1702
|
name: call.name,
|
|
1677
|
-
content: err.forModel()
|
|
1703
|
+
content: err.forModel()
|
|
1704
|
+
+ (err instanceof Declined ? '' : this.stuckNote(call, { err }))
|
|
1705
|
+
+ (this.declines >= 3
|
|
1706
|
+
? '\n\nThat is the third command declined in a row. They are not being refused one by one — '
|
|
1707
|
+
+ 'nothing can be run in this session at all, and the next one will be declined too. '
|
|
1708
|
+
+ 'Stop trying to run things: finish the work with the files you have, and say plainly '
|
|
1709
|
+
+ 'in your reply which steps someone will need to run themselves.'
|
|
1710
|
+
: ''),
|
|
1678
1711
|
});
|
|
1679
1712
|
return err.kind === 'bad_args';
|
|
1680
1713
|
}
|
|
@@ -1705,6 +1738,34 @@ export class Agent {
|
|
|
1705
1738
|
});
|
|
1706
1739
|
}
|
|
1707
1740
|
|
|
1741
|
+
// Reading the same unchanged file over and over.
|
|
1742
|
+
//
|
|
1743
|
+
// A traced build of a one-page app spent 53 of its 80 tool calls on
|
|
1744
|
+
// read_file, most of them the same handful of files again and again — six
|
|
1745
|
+
// seconds and a few thousand tokens each, for text that was already in the
|
|
1746
|
+
// conversation. The prompt has asked it not to since 1.27; asking is not
|
|
1747
|
+
// working, so the third read of a file nothing has written to since says
|
|
1748
|
+
// so instead of sending the file a third time.
|
|
1749
|
+
//
|
|
1750
|
+
// Two reads are left alone: the first is the work, and the second is
|
|
1751
|
+
// usually a fair re-check after an edit. Only the third is a loop. And it
|
|
1752
|
+
// is keyed on the file being untouched since — the moment anything writes
|
|
1753
|
+
// to it, the count starts again and a real re-read goes through.
|
|
1754
|
+
if (call.name === 'read_file' && call.args?.path) {
|
|
1755
|
+
const key = String(call.args.path);
|
|
1756
|
+
const seen = (this.reads ??= new Map()).get(key) ?? 0;
|
|
1757
|
+
if (seen >= 2 && !this.sinceCheck?.has(key)) {
|
|
1758
|
+
this.reads.set(key, seen + 1);
|
|
1759
|
+
return {
|
|
1760
|
+
content: `You have already read ${key} ${seen} times this turn and nothing has written to it since, ` +
|
|
1761
|
+
'so its contents are unchanged and already above. Use what is there. If you need a part ' +
|
|
1762
|
+
'you have lost, say which and read the files you still need together in one read_files.',
|
|
1763
|
+
summary: 'unchanged since you last read it',
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
this.reads.set(key, seen + 1);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1708
1769
|
if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
|
|
1709
1770
|
if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
|
|
1710
1771
|
if (call.name === 'delegate') return this.delegate(call.args?.tasks);
|
|
@@ -2036,7 +2097,7 @@ export class Agent {
|
|
|
2036
2097
|
async lookOnceThisTurn(root, changed) {
|
|
2037
2098
|
if (this.lookedThisTurn) return null;
|
|
2038
2099
|
|
|
2039
|
-
const pages = [...changed].filter((f) =>
|
|
2100
|
+
const pages = [...changed].filter((f) => PAGE.test(f));
|
|
2040
2101
|
if (!pages.length) return null;
|
|
2041
2102
|
this.lookedThisTurn = true;
|
|
2042
2103
|
|
|
@@ -2056,7 +2117,12 @@ export class Agent {
|
|
|
2056
2117
|
const html = pages.find((f) => /\.html?$/i.test(f));
|
|
2057
2118
|
if (!html) return null;
|
|
2058
2119
|
return await withStaticServer(path.dirname(path.resolve(root, html)), look);
|
|
2059
|
-
} catch {
|
|
2120
|
+
} catch (err) {
|
|
2121
|
+
// Say so, quietly, rather than skipping in silence. A browser that will
|
|
2122
|
+
// not start is a reason to carry on without the look — but a check that
|
|
2123
|
+
// stops running and never mentions it is worse than one that was never
|
|
2124
|
+
// written, because everything downstream still believes it ran.
|
|
2125
|
+
this.ui.note(`could not open the app to check it: ${String(err?.failed ?? err?.message ?? err).split('\n')[0]}`);
|
|
2060
2126
|
return null;
|
|
2061
2127
|
}
|
|
2062
2128
|
}
|