@miller-tech/uap 1.195.2 → 1.196.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/dist/.tsbuildinfo +1 -1
- package/dist/delivery/agentic-executor.d.ts +78 -1
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +248 -12
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
|
@@ -67,6 +67,41 @@ export declare function readFileWindow(contents: string, opts?: {
|
|
|
67
67
|
windowBytes?: number;
|
|
68
68
|
path?: string;
|
|
69
69
|
}): string;
|
|
70
|
+
/** Bytes a single `list_dir` / `run_bash` reply may occupy. */
|
|
71
|
+
export declare const TOOL_OUTPUT_MAX_BYTES = 4000;
|
|
72
|
+
/**
|
|
73
|
+
* One window of a directory listing, plus what it left out.
|
|
74
|
+
*
|
|
75
|
+
* Same defect as `read_file` had, in a different costume: `readdirSync(...)
|
|
76
|
+
* .join('\n').slice(0, 4000)` cut mid-name and said nothing, so a large
|
|
77
|
+
* directory was silently unlistable past its first ~200 entries and the agent
|
|
78
|
+
* had no way to learn the rest existed.
|
|
79
|
+
*
|
|
80
|
+
* Truncates on ENTRY boundaries — half a filename is worse than no filename,
|
|
81
|
+
* because the model will confidently `read_file` it — and takes the same
|
|
82
|
+
* 1-based `offset` as `read_file` so a big directory is reachable rather than
|
|
83
|
+
* merely honest about being cut.
|
|
84
|
+
*/
|
|
85
|
+
export declare function listingWindow(entries: readonly string[], opts?: {
|
|
86
|
+
offset?: number;
|
|
87
|
+
maxBytes?: number;
|
|
88
|
+
path?: string;
|
|
89
|
+
}): string;
|
|
90
|
+
/**
|
|
91
|
+
* A command's output, clipped from the MIDDLE rather than the end.
|
|
92
|
+
*
|
|
93
|
+
* `${stdout}${stderr}`.slice(0, 4000) keeps the head — which is the worst half
|
|
94
|
+
* to keep. A failing build puts its summary at the END ("error: could not
|
|
95
|
+
* compile", "3 failed, 41 passed", the exit line); head-only truncation hands
|
|
96
|
+
* the model a wall of compiler noise with the verdict cut off, and it cannot
|
|
97
|
+
* ask again because command output is not re-fetchable the way a file is.
|
|
98
|
+
*
|
|
99
|
+
* So both ends: the head carries the FIRST error (the one that usually causes
|
|
100
|
+
* the rest) and the invocation, the tail carries the verdict. The elision is
|
|
101
|
+
* stated in the middle where it happened, because a gap the reader cannot see
|
|
102
|
+
* is a gap the reader will read straight across.
|
|
103
|
+
*/
|
|
104
|
+
export declare function clipCommandOutput(output: string, maxBytes?: number): string;
|
|
70
105
|
/** Tool-call rounds before a final answer is forced (knob `UAP_MAX_TOOL_ROUNDS`). */
|
|
71
106
|
export declare function defaultMaxToolRounds(): number;
|
|
72
107
|
export interface AgenticExecutorOptions {
|
|
@@ -193,13 +228,17 @@ declare const TOOLS: readonly [{
|
|
|
193
228
|
readonly type: "function";
|
|
194
229
|
readonly function: {
|
|
195
230
|
readonly name: "list_dir";
|
|
196
|
-
readonly description:
|
|
231
|
+
readonly description: string;
|
|
197
232
|
readonly parameters: {
|
|
198
233
|
readonly type: "object";
|
|
199
234
|
readonly properties: {
|
|
200
235
|
readonly path: {
|
|
201
236
|
readonly type: "string";
|
|
202
237
|
};
|
|
238
|
+
readonly offset: {
|
|
239
|
+
readonly type: "integer";
|
|
240
|
+
readonly description: "1-based entry to start at. Omit for the start of the listing.";
|
|
241
|
+
};
|
|
203
242
|
};
|
|
204
243
|
readonly required: readonly ["path"];
|
|
205
244
|
};
|
|
@@ -319,6 +358,44 @@ declare const TOOLS: readonly [{
|
|
|
319
358
|
};
|
|
320
359
|
};
|
|
321
360
|
}];
|
|
361
|
+
/**
|
|
362
|
+
* Top-level definition HEADERS in a file, counted.
|
|
363
|
+
*
|
|
364
|
+
* Keyed on the whole header line, not the name, and that distinction is the
|
|
365
|
+
* whole design. Overloading is legal and common — Postgres `f(int)` beside
|
|
366
|
+
* `f(text)`, a TypeScript overload set — and those have DIFFERENT headers. An
|
|
367
|
+
* appended copy has an IDENTICAL one. Counting names would refuse the first and
|
|
368
|
+
* miss nothing extra; counting headers refuses only the copy.
|
|
369
|
+
*
|
|
370
|
+
* Column 0 only. A method inside a class or a nested helper is indented, and
|
|
371
|
+
* two classes may legitimately both define `run`. Top-level definitions are
|
|
372
|
+
* where re-adding is unambiguous.
|
|
373
|
+
*
|
|
374
|
+
* An unrecognised extension yields nothing, so the guard is inert rather than
|
|
375
|
+
* guessing at a language it does not know.
|
|
376
|
+
*/
|
|
377
|
+
export declare function topLevelDefinitionHeaders(content: string, path: string): Map<string, number>;
|
|
378
|
+
/** Escape hatch for a file that genuinely repeats a header (generated code). */
|
|
379
|
+
export declare function duplicateDefinitionGuardDisabled(): boolean;
|
|
380
|
+
/**
|
|
381
|
+
* Refuse a write that RE-ADDS a definition the file already has.
|
|
382
|
+
*
|
|
383
|
+
* The guards around this one all watch a file get smaller — anti-gutting
|
|
384
|
+
* (bytes), stub-substance (empty bodies), byte-identical NO-OP. Nothing watched
|
|
385
|
+
* it get BIGGER by repetition, and that is the shape the damage actually took.
|
|
386
|
+
*
|
|
387
|
+
* Live (cognition-engine, 2026-08-11): setup.sql ended the day holding 65 copies
|
|
388
|
+
* of `CREATE FUNCTION cognition.join_by_i_sql`, 63 of `join_by_i_time_sql`, and
|
|
389
|
+
* 40+ each of six more objects — 246 KB / 8,025 lines grown to 361 KB / 11,785,
|
|
390
|
+
* with 8,604 duplicate non-comment lines. A client relaunching the same "add X"
|
|
391
|
+
* mission dozens of times, against a file the agent could only see 3% of, will
|
|
392
|
+
* re-add X every time. Worse, every cheap check read as SUCCESS: the six lateral
|
|
393
|
+
* joins the mission targeted really were gone.
|
|
394
|
+
*
|
|
395
|
+
* Only the FIRST new duplicate is named. A refusal listing sixty of them is a
|
|
396
|
+
* wall the model will not act on.
|
|
397
|
+
*/
|
|
398
|
+
export declare function duplicateDefinitionRefusal(path: string, before: string, after: string): string | null;
|
|
322
399
|
/**
|
|
323
400
|
* Add an accepted contracts epic's files to the live contract lock, skipping
|
|
324
401
|
* files other machinery owns (manifests/lockfiles stay editable so later
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentic-executor.d.ts","sourceRoot":"","sources":["../../src/delivery/agentic-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAKzB;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAKxC;AAID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAClE,MAAM,CAwCR;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,MAAM,CAK7C;AA8ED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAE1E;AASD;;;;;;oEAMoE;AACpE,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAEhF;AAED,QAAA,MAAM,KAAK
|
|
1
|
+
{"version":3,"file":"agentic-executor.d.ts","sourceRoot":"","sources":["../../src/delivery/agentic-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAKzB;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAKxC;AAID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAClE,MAAM,CAwCR;AAED,+DAA+D;AAC/D,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,MAAM,CA6BR;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,MAA8B,GACvC,MAAM,CAWR;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,MAAM,CAK7C;AA8ED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAE1E;AASD;;;;;;oEAMoE;AACpE,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAEhF;AAED,QAAA,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqID,CAAC;AA2BX;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAmC5F;AA0CD,gFAAgF;AAChF,wBAAgB,gCAAgC,IAAI,OAAO,CAE1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CA0Bf;AAeD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAgBnG;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAE3E;AAqCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;OAMG;IACH,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAuC,GAC/D,MAAM,GAAG,SAAS,CA0CpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMnE;AAED,wBAAgB,YAAY,IAAI,SAAS,CAExC;AAWD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAyBf;AAED,wBAAgB,OAAO,CACrB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,EACnC,kBAAkB,EAAE,OAAO,EAC3B,SAAS,EAAE,OAAO,EAClB,aAAa,GAAE,WAAW,CAAC,MAAM,CAAa,EAI9C,KAAK,GAAE,SAA2B,GACjC,MAAM,CA6cR;AAED;;;;;GAKG;AACH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAKhE;AAsCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CA0B3E;AAYD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,GAChB,OAAO,GAAG,SAAS,CAGrB;AAuED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,sBAAsB,GAC3B,YAAY,CAoYd;AAED;;;;GAIG;AAEH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB3E;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAIxD"}
|
|
@@ -118,6 +118,77 @@ export function readFileWindow(contents, opts = {}) {
|
|
|
118
118
|
return (`${body}\n\n… showing lines ${startLine}-${endLine} of ${totalLines} ` +
|
|
119
119
|
`(${contents.length} bytes total).${more}`);
|
|
120
120
|
}
|
|
121
|
+
/** Bytes a single `list_dir` / `run_bash` reply may occupy. */
|
|
122
|
+
export const TOOL_OUTPUT_MAX_BYTES = 4_000;
|
|
123
|
+
/**
|
|
124
|
+
* One window of a directory listing, plus what it left out.
|
|
125
|
+
*
|
|
126
|
+
* Same defect as `read_file` had, in a different costume: `readdirSync(...)
|
|
127
|
+
* .join('\n').slice(0, 4000)` cut mid-name and said nothing, so a large
|
|
128
|
+
* directory was silently unlistable past its first ~200 entries and the agent
|
|
129
|
+
* had no way to learn the rest existed.
|
|
130
|
+
*
|
|
131
|
+
* Truncates on ENTRY boundaries — half a filename is worse than no filename,
|
|
132
|
+
* because the model will confidently `read_file` it — and takes the same
|
|
133
|
+
* 1-based `offset` as `read_file` so a big directory is reachable rather than
|
|
134
|
+
* merely honest about being cut.
|
|
135
|
+
*/
|
|
136
|
+
export function listingWindow(entries, opts = {}) {
|
|
137
|
+
const maxBytes = Math.max(1, opts.maxBytes ?? TOOL_OUTPUT_MAX_BYTES);
|
|
138
|
+
const total = entries.length;
|
|
139
|
+
const rawOffset = Number(opts.offset);
|
|
140
|
+
const start = Number.isFinite(rawOffset) ? Math.max(1, Math.trunc(rawOffset)) : 1;
|
|
141
|
+
if (total === 0)
|
|
142
|
+
return '';
|
|
143
|
+
if (start > total) {
|
|
144
|
+
return `NOTE: offset ${start} is past the end of ${opts.path ?? 'this directory'} (${total} entries).`;
|
|
145
|
+
}
|
|
146
|
+
const shown = [];
|
|
147
|
+
let used = 0;
|
|
148
|
+
for (const entry of entries.slice(start - 1)) {
|
|
149
|
+
const cost = entry.length + 1;
|
|
150
|
+
if (shown.length > 0 && used + cost > maxBytes)
|
|
151
|
+
break;
|
|
152
|
+
shown.push(entry);
|
|
153
|
+
used += cost;
|
|
154
|
+
}
|
|
155
|
+
const end = start + shown.length - 1;
|
|
156
|
+
const body = shown.join('\n');
|
|
157
|
+
if (start === 1 && end >= total)
|
|
158
|
+
return body;
|
|
159
|
+
const next = end + 1;
|
|
160
|
+
const more = next <= total
|
|
161
|
+
? ` Call list_dir with {"path":"${opts.path ?? '.'}","offset":${next}} for the rest.`
|
|
162
|
+
: '';
|
|
163
|
+
return `${body}\n\n… showing entries ${start}-${end} of ${total}.${more}`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A command's output, clipped from the MIDDLE rather than the end.
|
|
167
|
+
*
|
|
168
|
+
* `${stdout}${stderr}`.slice(0, 4000) keeps the head — which is the worst half
|
|
169
|
+
* to keep. A failing build puts its summary at the END ("error: could not
|
|
170
|
+
* compile", "3 failed, 41 passed", the exit line); head-only truncation hands
|
|
171
|
+
* the model a wall of compiler noise with the verdict cut off, and it cannot
|
|
172
|
+
* ask again because command output is not re-fetchable the way a file is.
|
|
173
|
+
*
|
|
174
|
+
* So both ends: the head carries the FIRST error (the one that usually causes
|
|
175
|
+
* the rest) and the invocation, the tail carries the verdict. The elision is
|
|
176
|
+
* stated in the middle where it happened, because a gap the reader cannot see
|
|
177
|
+
* is a gap the reader will read straight across.
|
|
178
|
+
*/
|
|
179
|
+
export function clipCommandOutput(output, maxBytes = TOOL_OUTPUT_MAX_BYTES) {
|
|
180
|
+
if (output.length <= maxBytes)
|
|
181
|
+
return output;
|
|
182
|
+
const budget = Math.max(1, maxBytes);
|
|
183
|
+
// Tail-weighted: the verdict is worth more than the preamble, but the first
|
|
184
|
+
// error still has to survive.
|
|
185
|
+
const headBytes = Math.floor(budget * 0.4);
|
|
186
|
+
const tailBytes = budget - headBytes;
|
|
187
|
+
const head = output.slice(0, headBytes);
|
|
188
|
+
const tail = output.slice(output.length - tailBytes);
|
|
189
|
+
const elided = output.length - head.length - tail.length;
|
|
190
|
+
return `${head}\n\n… [${elided} bytes of output elided — this is the START and the END of ${output.length} bytes] …\n\n${tail}`;
|
|
191
|
+
}
|
|
121
192
|
/** Tool-call rounds before a final answer is forced (knob `UAP_MAX_TOOL_ROUNDS`). */
|
|
122
193
|
export function defaultMaxToolRounds() {
|
|
123
194
|
const set = (process.env.UAP_MAX_TOOL_ROUNDS ?? '').trim();
|
|
@@ -251,10 +322,17 @@ const TOOLS = [
|
|
|
251
322
|
type: 'function',
|
|
252
323
|
function: {
|
|
253
324
|
name: 'list_dir',
|
|
254
|
-
description: 'List files/dirs at a path relative to the project root.'
|
|
325
|
+
description: 'List files/dirs at a path relative to the project root. A large directory comes ' +
|
|
326
|
+
'back one window at a time and the reply says so — pass offset (1-based entry) for the rest.',
|
|
255
327
|
parameters: {
|
|
256
328
|
type: 'object',
|
|
257
|
-
properties: {
|
|
329
|
+
properties: {
|
|
330
|
+
path: { type: 'string' },
|
|
331
|
+
offset: {
|
|
332
|
+
type: 'integer',
|
|
333
|
+
description: '1-based entry to start at. Omit for the start of the listing.',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
258
336
|
required: ['path'],
|
|
259
337
|
},
|
|
260
338
|
},
|
|
@@ -371,6 +449,149 @@ function editGuttingRefusal(path, before, after) {
|
|
|
371
449
|
'that removes most of the file. If you meant to delete code, do it in smaller, ' +
|
|
372
450
|
'reviewable edits; if you meant to replace it, send the replacement body.');
|
|
373
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Top-level definition HEADERS in a file, counted.
|
|
454
|
+
*
|
|
455
|
+
* Keyed on the whole header line, not the name, and that distinction is the
|
|
456
|
+
* whole design. Overloading is legal and common — Postgres `f(int)` beside
|
|
457
|
+
* `f(text)`, a TypeScript overload set — and those have DIFFERENT headers. An
|
|
458
|
+
* appended copy has an IDENTICAL one. Counting names would refuse the first and
|
|
459
|
+
* miss nothing extra; counting headers refuses only the copy.
|
|
460
|
+
*
|
|
461
|
+
* Column 0 only. A method inside a class or a nested helper is indented, and
|
|
462
|
+
* two classes may legitimately both define `run`. Top-level definitions are
|
|
463
|
+
* where re-adding is unambiguous.
|
|
464
|
+
*
|
|
465
|
+
* An unrecognised extension yields nothing, so the guard is inert rather than
|
|
466
|
+
* guessing at a language it does not know.
|
|
467
|
+
*/
|
|
468
|
+
export function topLevelDefinitionHeaders(content, path) {
|
|
469
|
+
const ext = (path.split('.').pop() ?? '').toLowerCase();
|
|
470
|
+
const patterns = {
|
|
471
|
+
sql: [
|
|
472
|
+
/^CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?(?:FUNCTION|PROCEDURE|VIEW|TABLE|TYPE|INDEX)\b.*/i,
|
|
473
|
+
],
|
|
474
|
+
ts: [TS_DEF, TS_CONST],
|
|
475
|
+
tsx: [TS_DEF, TS_CONST],
|
|
476
|
+
js: [TS_DEF, TS_CONST],
|
|
477
|
+
jsx: [TS_DEF, TS_CONST],
|
|
478
|
+
mjs: [TS_DEF, TS_CONST],
|
|
479
|
+
py: [/^(?:async\s+)?(?:def|class)\s+\w+.*/],
|
|
480
|
+
rs: [/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:fn|struct|enum|trait)\s+\w+.*/],
|
|
481
|
+
go: [/^func\s+\w+.*/, /^type\s+\w+\s+.*/],
|
|
482
|
+
}[ext];
|
|
483
|
+
if (!patterns)
|
|
484
|
+
return new Map();
|
|
485
|
+
const counts = new Map();
|
|
486
|
+
const lines = content.split('\n');
|
|
487
|
+
for (let i = 0; i < lines.length; i++) {
|
|
488
|
+
const raw = lines[i];
|
|
489
|
+
// Column 0: no leading whitespace.
|
|
490
|
+
if (raw !== raw.trimStart())
|
|
491
|
+
continue;
|
|
492
|
+
const line = raw.trim();
|
|
493
|
+
// No comment filter: every pattern is anchored at ^, so `-- CREATE …`,
|
|
494
|
+
// `// export function …` and `# def …` cannot match in the first place.
|
|
495
|
+
// A filter here survived mutation because it could never change an outcome.
|
|
496
|
+
if (!line)
|
|
497
|
+
continue;
|
|
498
|
+
for (const re of patterns) {
|
|
499
|
+
if (!re.test(line))
|
|
500
|
+
continue;
|
|
501
|
+
counts.set(signatureKey(lines, i), (counts.get(signatureKey(lines, i)) ?? 0) + 1);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return counts;
|
|
506
|
+
}
|
|
507
|
+
/** How many lines of a wrapped signature to gather before giving up. */
|
|
508
|
+
const SIGNATURE_MAX_LINES = 24;
|
|
509
|
+
/**
|
|
510
|
+
* The WHOLE signature, not just the line the definition starts on.
|
|
511
|
+
*
|
|
512
|
+
* Found by running this over the repo itself: `src/benchmarks/model-integration.ts`
|
|
513
|
+
* declares `runModelBenchmark` three times — a legitimate TypeScript overload
|
|
514
|
+
* set — and two of those start with the identical line
|
|
515
|
+
* `export async function runModelBenchmark(`, because the parameters wrap. Keying
|
|
516
|
+
* on that first line alone would call a real overload a duplicate and refuse a
|
|
517
|
+
* legitimate write. The parameters are exactly what distinguishes overloads, so
|
|
518
|
+
* the key has to reach them.
|
|
519
|
+
*
|
|
520
|
+
* Gathers until the parentheses balance, bounded, so a malformed or enormous
|
|
521
|
+
* signature degrades to "just this line" instead of swallowing the file.
|
|
522
|
+
*/
|
|
523
|
+
function signatureKey(lines, start) {
|
|
524
|
+
let text = lines[start].trim();
|
|
525
|
+
let depth = countParens(text);
|
|
526
|
+
for (let j = start + 1; depth > 0 && j < lines.length && j - start < SIGNATURE_MAX_LINES; j++) {
|
|
527
|
+
text += ` ${lines[j].trim()}`;
|
|
528
|
+
depth += countParens(lines[j]);
|
|
529
|
+
}
|
|
530
|
+
// Collapse whitespace so reformatting is not read as a different signature,
|
|
531
|
+
// and drop a trailing brace/semicolon so `fn f() {` matches `fn f()`.
|
|
532
|
+
return text.replace(/\s+/g, ' ').replace(/[{;]\s*$/, '').trim();
|
|
533
|
+
}
|
|
534
|
+
function countParens(s) {
|
|
535
|
+
let d = 0;
|
|
536
|
+
for (const ch of s) {
|
|
537
|
+
if (ch === '(')
|
|
538
|
+
d += 1;
|
|
539
|
+
else if (ch === ')')
|
|
540
|
+
d -= 1;
|
|
541
|
+
}
|
|
542
|
+
return d;
|
|
543
|
+
}
|
|
544
|
+
const TS_DEF = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class)\s+\w+.*/;
|
|
545
|
+
const TS_CONST = /^(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?(?:\(|function\b|class\b).*/;
|
|
546
|
+
/** Escape hatch for a file that genuinely repeats a header (generated code). */
|
|
547
|
+
export function duplicateDefinitionGuardDisabled() {
|
|
548
|
+
return process.env.UAP_DELIVER_ALLOW_DUPLICATE_DEFS === '1';
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Refuse a write that RE-ADDS a definition the file already has.
|
|
552
|
+
*
|
|
553
|
+
* The guards around this one all watch a file get smaller — anti-gutting
|
|
554
|
+
* (bytes), stub-substance (empty bodies), byte-identical NO-OP. Nothing watched
|
|
555
|
+
* it get BIGGER by repetition, and that is the shape the damage actually took.
|
|
556
|
+
*
|
|
557
|
+
* Live (cognition-engine, 2026-08-11): setup.sql ended the day holding 65 copies
|
|
558
|
+
* of `CREATE FUNCTION cognition.join_by_i_sql`, 63 of `join_by_i_time_sql`, and
|
|
559
|
+
* 40+ each of six more objects — 246 KB / 8,025 lines grown to 361 KB / 11,785,
|
|
560
|
+
* with 8,604 duplicate non-comment lines. A client relaunching the same "add X"
|
|
561
|
+
* mission dozens of times, against a file the agent could only see 3% of, will
|
|
562
|
+
* re-add X every time. Worse, every cheap check read as SUCCESS: the six lateral
|
|
563
|
+
* joins the mission targeted really were gone.
|
|
564
|
+
*
|
|
565
|
+
* Only the FIRST new duplicate is named. A refusal listing sixty of them is a
|
|
566
|
+
* wall the model will not act on.
|
|
567
|
+
*/
|
|
568
|
+
export function duplicateDefinitionRefusal(path, before, after) {
|
|
569
|
+
if (duplicateDefinitionGuardDisabled())
|
|
570
|
+
return null;
|
|
571
|
+
const now = topLevelDefinitionHeaders(after, path);
|
|
572
|
+
// Fast path only — the loop below is already a no-op on an empty map. It
|
|
573
|
+
// earns its place by skipping the second parse, not by changing an outcome,
|
|
574
|
+
// which is why mutating it away leaves every test passing.
|
|
575
|
+
if (now.size === 0)
|
|
576
|
+
return null;
|
|
577
|
+
const was = topLevelDefinitionHeaders(before, path);
|
|
578
|
+
for (const [header, count] of now) {
|
|
579
|
+
const prior = was.get(header) ?? 0;
|
|
580
|
+
// Adding a brand-new definition (0 -> 1) is the normal case. Editing one in
|
|
581
|
+
// place (1 -> 1) is fine. Going 1 -> 2 is re-adding what is already there.
|
|
582
|
+
if (count <= prior || count < 2)
|
|
583
|
+
continue;
|
|
584
|
+
const firstLine = before.split('\n').findIndex((l) => l === l.trimStart() && l.trim().replace(/\s+/g, ' ').replace(/[{;]\s*$/, '').trim() === header);
|
|
585
|
+
const where = firstLine >= 0 ? ` at line ${firstLine + 1}` : '';
|
|
586
|
+
return (`ERROR: refusing to write ${path} — it would add a SECOND copy of a definition the file ` +
|
|
587
|
+
`already contains${where}:\n\n ${header.slice(0, 160)}\n\n` +
|
|
588
|
+
`The file already has this (${prior} ${prior === 1 ? 'copy' : 'copies'}; this write makes ${count}). ` +
|
|
589
|
+
`Do not append it again — use edit_file to change the existing one in place, or read the file ` +
|
|
590
|
+
`first to see what is already there. If this file genuinely repeats that header, set ` +
|
|
591
|
+
`UAP_DELIVER_ALLOW_DUPLICATE_DEFS=1.`);
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
374
595
|
/** Resolve a model-supplied path inside the project root, refusing escapes. */
|
|
375
596
|
function safePath(projectRoot, p) {
|
|
376
597
|
const abs = isAbsolute(p) ? p : resolve(projectRoot, p);
|
|
@@ -584,7 +805,9 @@ export function repeatReadNote(cache, projectRoot, name, args, round) {
|
|
|
584
805
|
// page 1, and scolding a model for paging through a long file would push it
|
|
585
806
|
// straight back into the read-loop this is meant to end. Only a genuine
|
|
586
807
|
// repeat — same path, same window, unchanged on disk — earns the note.
|
|
587
|
-
|
|
808
|
+
// Both windowed tools, not just read_file: list_dir pages too now, and a
|
|
809
|
+
// directory's second page is no more a re-read than a file's.
|
|
810
|
+
const offsetKey = Number.isFinite(Number(args.offset))
|
|
588
811
|
? `@${Math.max(1, Math.trunc(Number(args.offset)))}`
|
|
589
812
|
: '';
|
|
590
813
|
const key = `${name}:${args.path}${offsetKey}`;
|
|
@@ -641,11 +864,9 @@ sweep = disabledSweep()) {
|
|
|
641
864
|
// see what is in there. Serve that intent instead of failing, and name the
|
|
642
865
|
// right tool for next time. A wasted turn becomes a useful one.
|
|
643
866
|
if (statSync(abs).isDirectory()) {
|
|
644
|
-
const entries = readdirSync(abs)
|
|
867
|
+
const entries = listingWindow(readdirSync(abs)
|
|
645
868
|
.filter((e) => !AGENT_INTERNAL_DIRS.includes(e))
|
|
646
|
-
.map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e))
|
|
647
|
-
.join('\n')
|
|
648
|
-
.slice(0, 4000);
|
|
869
|
+
.map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e)), { path: String(args.path) });
|
|
649
870
|
return (`NOTE: '${String(args.path)}' is a DIRECTORY, not a file — use list_dir for it. ` +
|
|
650
871
|
`Its contents:\n${entries}`);
|
|
651
872
|
}
|
|
@@ -661,13 +882,11 @@ sweep = disabledSweep()) {
|
|
|
661
882
|
return internal;
|
|
662
883
|
if (!existsSync(abs))
|
|
663
884
|
return `ERROR: not found: ${args.path}`;
|
|
664
|
-
return readdirSync(abs)
|
|
885
|
+
return listingWindow(readdirSync(abs)
|
|
665
886
|
// Also HIDE the internal dirs from any listing (e.g. the project root), so
|
|
666
887
|
// the agent never even sees `.uap/` and is not tempted to spelunk it.
|
|
667
888
|
.filter((e) => !AGENT_INTERNAL_DIRS.includes(e))
|
|
668
|
-
.map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e))
|
|
669
|
-
.join('\n')
|
|
670
|
-
.slice(0, 4000);
|
|
889
|
+
.map((e) => (statSync(join(abs, e)).isDirectory() ? `${e}/` : e)), { offset: args.offset, path: String(args.path ?? '.') });
|
|
671
890
|
}
|
|
672
891
|
if (name === 'write_file') {
|
|
673
892
|
const abs = safePath(projectRoot, String(args.path));
|
|
@@ -774,6 +993,17 @@ sweep = disabledSweep()) {
|
|
|
774
993
|
if (stub.isStub)
|
|
775
994
|
return stubRefusal(String(args.path), stub);
|
|
776
995
|
}
|
|
996
|
+
// Growth by repetition. Ordered LAST among the write guards: it is the
|
|
997
|
+
// only one that reads the whole prior file for structure, so it never
|
|
998
|
+
// pays that cost for a write another guard has already refused.
|
|
999
|
+
if (existsSync(abs)) {
|
|
1000
|
+
try {
|
|
1001
|
+
const dup = duplicateDefinitionRefusal(String(args.path), readFileSync(abs, 'utf-8'), String(args.content ?? ''));
|
|
1002
|
+
if (dup)
|
|
1003
|
+
return dup;
|
|
1004
|
+
}
|
|
1005
|
+
catch { /* unreadable — a guard that cannot read cannot judge */ }
|
|
1006
|
+
}
|
|
777
1007
|
mkdirSync(dirname(abs), { recursive: true });
|
|
778
1008
|
writeFileSync(abs, String(args.content ?? ''), 'utf-8');
|
|
779
1009
|
recordAuthorisedWrite(sweep, rel, String(args.content ?? ''));
|
|
@@ -903,6 +1133,12 @@ sweep = disabledSweep()) {
|
|
|
903
1133
|
if (stub.isStub)
|
|
904
1134
|
return stubRefusal(String(args.path), stub);
|
|
905
1135
|
}
|
|
1136
|
+
// An edit can append just as easily as a write can — an old_string anchored
|
|
1137
|
+
// at the end of the file with the whole definition in new_string is exactly
|
|
1138
|
+
// how a file grows a second copy.
|
|
1139
|
+
const dupEdit = duplicateDefinitionRefusal(String(args.path), current, updated);
|
|
1140
|
+
if (dupEdit)
|
|
1141
|
+
return dupEdit;
|
|
906
1142
|
writeFileSync(abs, updated, 'utf-8');
|
|
907
1143
|
recordAuthorisedWrite(sweep, rel, updated);
|
|
908
1144
|
const rustCheckNote = maybeRustWriteCheck(projectRoot, rel);
|
|
@@ -1017,7 +1253,7 @@ sweep = disabledSweep()) {
|
|
|
1017
1253
|
/* best effort */
|
|
1018
1254
|
}
|
|
1019
1255
|
}
|
|
1020
|
-
const out = `${r.stdout ?? ''}${r.stderr ?? ''}
|
|
1256
|
+
const out = clipCommandOutput(`${r.stdout ?? ''}${r.stderr ?? ''}`);
|
|
1021
1257
|
const note = (restored.length > 0
|
|
1022
1258
|
? `\n[blocked: restored ${restored.length} protected file(s) your command modified]`
|
|
1023
1259
|
: '') +
|