bare-agent 0.26.2 → 0.27.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/README.md +7 -1
- package/bareagent.context.md +47 -2
- package/package.json +1 -1
- package/src/loop.d.ts +40 -0
- package/src/loop.js +185 -15
- package/src/provider-anthropic.d.ts +32 -2
- package/src/provider-anthropic.js +106 -4
- package/src/provider-gemini.js +10 -0
- package/src/provider-ollama.d.ts +1 -1
- package/src/provider-ollama.js +32 -9
- package/src/provider-openai.js +14 -5
- package/src/provider-stop-reason.d.ts +26 -0
- package/src/provider-stop-reason.js +148 -0
- package/src/recurse-synthesize.js +11 -2
- package/tools/shell.d.ts +13 -2
- package/tools/shell.js +28 -8
- package/types/index.d.ts +46 -0
package/tools/shell.js
CHANGED
|
@@ -90,23 +90,40 @@ async function readEntry(rawPath, maxBytes) {
|
|
|
90
90
|
* writes through the shell is impractical: redirection is a shell metachar that an argv/bash allowlist denies).
|
|
91
91
|
* Creates parent directories. Caps size as a sanity ceiling. NO shell — so it gates cleanly through bareguard's
|
|
92
92
|
* fs primitive when the adopter translates `shell_write` → `{ type:'write', path }` (see createShellTools doc).
|
|
93
|
-
*
|
|
93
|
+
*
|
|
94
|
+
* `content` is REQUIRED and must be a string (BA-4). It used to default to `''`, which made the ordinary
|
|
95
|
+
* failure mode of a long generation — the model hits its output-token cap and the tool call arrives with
|
|
96
|
+
* `content` absent — silently truncate the target to zero bytes and report `"wrote 0 bytes"` as SUCCESS.
|
|
97
|
+
* No policy can catch that: a 0-byte write is a legal write, and bareguard's fs primitive judges
|
|
98
|
+
* `{type:'write', path}` without ever inspecting the body. It is a missing precondition in the primitive,
|
|
99
|
+
* not a governance gap. An explicit `content: ''` still empties the file — the caller meant it.
|
|
100
|
+
* `content` is typed REQUIRED so the generated `.d.ts` states the real contract — a library caller that omits
|
|
101
|
+
* it is a type error, not a runtime surprise. The guard below still runs, because the tool-execute boundary
|
|
102
|
+
* feeds this UNTRUSTED model-authored args (that is the boundary BA-4 was breached at, and where types buy
|
|
103
|
+
* nothing).
|
|
104
|
+
* @param {{path: string, content: string, append?: boolean, maxBytes?: number}} args
|
|
94
105
|
* @returns {Promise<string>}
|
|
95
106
|
*/
|
|
96
|
-
async function writeFile({ path: rawPath, content
|
|
107
|
+
async function writeFile({ path: rawPath, content, append = false, maxBytes }) {
|
|
97
108
|
if (typeof rawPath !== 'string' || rawPath.length === 0) {
|
|
98
109
|
throw new Error('shell_write requires a non-empty "path" string');
|
|
99
110
|
}
|
|
100
|
-
|
|
111
|
+
if (typeof content !== 'string') {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'shell_write requires a "content" string (pass content:"" to deliberately empty the file). '
|
|
114
|
+
+ `Got ${content === undefined ? 'no content argument' : `content of type ${content === null ? 'null' : typeof content}`}`
|
|
115
|
+
+ ' — refusing to write, the file is unchanged. If your output was cut short, retry with the full content.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
101
118
|
const cap = maxBytes || DEFAULT_WRITE_MAX_BYTES;
|
|
102
|
-
const bytes = Buffer.byteLength(
|
|
119
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
103
120
|
if (bytes > cap) {
|
|
104
121
|
throw new Error(`shell_write content is ${bytes} bytes, over the ${cap}-byte cap (pass maxBytes to raise it)`);
|
|
105
122
|
}
|
|
106
123
|
const resolved = path.resolve(expandHome(rawPath));
|
|
107
124
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
108
|
-
if (append) await fs.appendFile(resolved,
|
|
109
|
-
else await fs.writeFile(resolved,
|
|
125
|
+
if (append) await fs.appendFile(resolved, content, 'utf8');
|
|
126
|
+
else await fs.writeFile(resolved, content, 'utf8');
|
|
110
127
|
return `${append ? 'appended' : 'wrote'} ${bytes} bytes to ${resolved}`;
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -425,13 +442,16 @@ function createShellTools() {
|
|
|
425
442
|
type: 'object',
|
|
426
443
|
properties: {
|
|
427
444
|
path: { type: 'string', description: 'Target file path. ~ expands to home. Parent dirs are created.' },
|
|
428
|
-
content: { type: 'string', description: 'The full text to write (UTF-8).' },
|
|
445
|
+
content: { type: 'string', description: 'The full text to write (UTF-8). Required — a call without it is REJECTED, not treated as an empty write. Pass "" only to deliberately empty the file.' },
|
|
429
446
|
append: { type: 'boolean', description: 'Append to the file instead of overwriting it (default false).' },
|
|
430
447
|
maxBytes: { type: 'integer', description: 'Reject a write larger than this many bytes (default 5242880).' },
|
|
431
448
|
},
|
|
432
449
|
required: ['path', 'content'],
|
|
433
450
|
},
|
|
434
|
-
|
|
451
|
+
// The args are model-authored and UNTRUSTED — `content` may be absent (an output-token-capped
|
|
452
|
+
// generation), so the boundary type stays loose and `writeFile` enforces the contract at runtime (BA-4).
|
|
453
|
+
execute: async (/** @type {{path: string, content?: string, append?: boolean, maxBytes?: number}} */ args) =>
|
|
454
|
+
writeFile(/** @type {any} */ (args)),
|
|
435
455
|
},
|
|
436
456
|
{
|
|
437
457
|
name: 'shell_run',
|
package/types/index.d.ts
CHANGED
|
@@ -80,12 +80,39 @@ export interface GenerateResult {
|
|
|
80
80
|
usage: Usage;
|
|
81
81
|
/** Model id the response was produced by; preferred over Provider.model for cost accounting. */
|
|
82
82
|
model?: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Why generation ended, normalized across providers (BA-6). Before this, no provider read its native
|
|
85
|
+
* finish-reason field, so a round the API CUT OFF at the token cap was indistinguishable from one the
|
|
86
|
+
* model chose to end — and the Loop, whose rule is "no tool calls ⇒ final answer", returned the
|
|
87
|
+
* truncation as a clean finish with `error: null`.
|
|
88
|
+
*
|
|
89
|
+
* - `'end_turn'` — the model finished on its own. The only clean finish.
|
|
90
|
+
* - `'max_tokens'` — CUT OFF at the output cap. The Loop returns `error: 'truncated:max_tokens'`
|
|
91
|
+
* (preserving the partial text) and REFUSES to execute any tool call the round carries: a complete
|
|
92
|
+
* call always arrives as `'tool_use'`, so one riding a `'max_tokens'` round was cut off
|
|
93
|
+
* mid-generation with arguments missing — the BA-4 file-zeroing mechanism.
|
|
94
|
+
* - `'tool_use'` — stopped to call a tool, and the call is COMPLETE.
|
|
95
|
+
* - `'stop_sequence'` / `'refusal'` / `'pause_turn'` / `'context_exceeded'` — reported, not acted on.
|
|
96
|
+
* - `null` — the provider didn't report one (e.g. CLIPipe) or the value is unrecognized. Reproduces
|
|
97
|
+
* pre-BA-6 behavior exactly, so an unmapped provider degrades to the status quo.
|
|
98
|
+
*/
|
|
99
|
+
stopReason?: string | null;
|
|
83
100
|
/**
|
|
84
101
|
* True when the requested `temperature` was rejected by the model (400, unsupported/deprecated) and
|
|
85
102
|
* the request was retried without it (BA-10). The response was produced at the model's DEFAULT
|
|
86
103
|
* temperature, not the one requested — callers reporting an effective temperature must honor this.
|
|
87
104
|
*/
|
|
88
105
|
temperatureDropped?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* BA-7 — provider-native content blocks the normalized `{text, toolCalls}` shape cannot express
|
|
108
|
+
* (Anthropic `thinking` / `redacted_thinking`), captured opaquely so the Loop can put them on the
|
|
109
|
+
* transcript and the provider can replay them on the next round.
|
|
110
|
+
*
|
|
111
|
+
* Present only when the response actually carried such blocks — so a provider that returns none
|
|
112
|
+
* leaves both the result and the resulting message byte-identical to pre-BA-7. See `Message.providerBlocks`
|
|
113
|
+
* for the replay contract (the provider/model tag is enforced; a signature is model-bound).
|
|
114
|
+
*/
|
|
115
|
+
providerBlocks?: { provider: string; model: string; blocks: any[] };
|
|
89
116
|
/**
|
|
90
117
|
* Authoritative per-call cost in USD, reported by the provider itself — e.g. CLIPipeProvider
|
|
91
118
|
* `parse:'claude-json'` surfacing the claude CLI's own `total_cost_usd`, a real price with no local
|
|
@@ -102,6 +129,25 @@ export interface Message {
|
|
|
102
129
|
content?: string | null;
|
|
103
130
|
tool_calls?: any[];
|
|
104
131
|
tool_call_id?: string;
|
|
132
|
+
/**
|
|
133
|
+
* BA-7 — provider-native content blocks that this OpenAI-shaped message cannot express, carried
|
|
134
|
+
* verbatim so they can be replayed to the provider that issued them.
|
|
135
|
+
*
|
|
136
|
+
* Today this is Anthropic `thinking` / `redacted_thinking`. Anthropic's contract is that such
|
|
137
|
+
* blocks are echoed back UNCHANGED (`signature` included) when continuing a tool-use conversation;
|
|
138
|
+
* before BA-7 there was no field on this type that could hold one, so they were silently dropped.
|
|
139
|
+
*
|
|
140
|
+
* OPAQUE by design — the Loop never reads `blocks`, and the provider re-emits their bytes rather
|
|
141
|
+
* than re-serializing a parsed shape (a `redacted_thinking` block cannot survive a round-trip
|
|
142
|
+
* through parsed fields). The `provider`/`model` tag is enforced on replay: a thinking signature is
|
|
143
|
+
* bound to the model that produced it, so a mismatch drops the blocks and degrades to the lossy
|
|
144
|
+
* pre-BA-7 request rather than risking a 400.
|
|
145
|
+
*
|
|
146
|
+
* The normalized `content` / `tool_calls` remain the source of truth: only blocks that have no
|
|
147
|
+
* normalized representation live here, so an `assemble`/`trim` seam that rewrites this message is
|
|
148
|
+
* never silently undone by a stale cached copy of its text.
|
|
149
|
+
*/
|
|
150
|
+
providerBlocks?: { provider: string; model: string; blocks: any[] };
|
|
105
151
|
[key: string]: any;
|
|
106
152
|
}
|
|
107
153
|
|