@semalt-ai/code 1.8.5 → 1.20.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/.claude/settings.local.json +7 -1
- package/.github/workflows/ci.yml +69 -0
- package/ARCHITECTURE.md +6 -95
- package/CLAUDE.md +196 -316
- package/README.md +148 -4
- package/docs/ARCHITECTURE.md +1321 -0
- package/docs/CONFIG.md +340 -0
- package/docs/HISTORY.md +245 -0
- package/examples/embed.js +74 -0
- package/index.js +251 -10
- package/lib/agent.js +856 -120
- package/lib/api.js +239 -50
- package/lib/args.js +74 -2
- package/lib/audit.js +23 -1
- package/lib/background.js +584 -0
- package/lib/checkpoints.js +757 -0
- package/lib/commands/auth.js +94 -0
- package/lib/commands/chat-session.js +489 -0
- package/lib/commands/chat-slash.js +415 -0
- package/lib/commands/chat-turn.js +669 -0
- package/lib/commands/chat.js +407 -0
- package/lib/commands/custom.js +157 -0
- package/lib/commands/history-utils.js +66 -0
- package/lib/commands/index.js +268 -0
- package/lib/commands/mcp.js +113 -0
- package/lib/commands/oneshot.js +193 -0
- package/lib/commands/registry.js +269 -0
- package/lib/commands/tasks.js +89 -0
- package/lib/compact.js +87 -0
- package/lib/config.js +360 -11
- package/lib/constants.js +401 -3
- package/lib/deny.js +199 -0
- package/lib/doctor.js +160 -0
- package/lib/headless.js +202 -0
- package/lib/hooks.js +286 -0
- package/lib/images.js +270 -0
- package/lib/internals.js +49 -0
- package/lib/mcp/boundary.js +131 -0
- package/lib/mcp/client.js +270 -0
- package/lib/mcp/oauth.js +134 -0
- package/lib/memory.js +209 -0
- package/lib/metrics.js +37 -2
- package/lib/payload.js +54 -0
- package/lib/permission-rules.js +401 -0
- package/lib/permissions.js +123 -26
- package/lib/pricing.js +67 -0
- package/lib/proc.js +62 -0
- package/lib/prompts.js +99 -8
- package/lib/sandbox.js +568 -0
- package/lib/sdk.js +328 -0
- package/lib/secrets.js +211 -0
- package/lib/skills.js +223 -0
- package/lib/subagents.js +516 -0
- package/lib/tool_registry.js +2862 -0
- package/lib/tool_specs.js +263 -9
- package/lib/tools.js +352 -1039
- package/lib/ui/anim.js +86 -0
- package/lib/ui/ansi.js +17 -27
- package/lib/ui/chat-history.js +253 -71
- package/lib/ui/create-ui.js +67 -24
- package/lib/ui/diff.js +90 -25
- package/lib/ui/file-activity.js +236 -0
- package/lib/ui/format.js +195 -29
- package/lib/ui/input-field.js +21 -11
- package/lib/ui/md-stream.js +234 -0
- package/lib/ui/render-operation.js +113 -0
- package/lib/ui/select.js +1 -4
- package/lib/ui/status-bar.js +146 -36
- package/lib/ui/stream.js +20 -13
- package/lib/ui/theme.js +190 -44
- package/lib/ui/tool-operation.js +190 -0
- package/lib/ui/utils.js +9 -5
- package/lib/ui/web-activity.js +270 -0
- package/lib/ui/writer.js +159 -45
- package/lib/ui.js +1 -1
- package/lib/verify.js +229 -0
- package/lib/web-extract.js +213 -0
- package/lib/web-summarize.js +68 -0
- package/package.json +19 -4
- package/scripts/lint.js +57 -0
- package/test/agent-loop.test.js +389 -0
- package/test/anim-driver.test.js +153 -0
- package/test/ask-user-display.test.js +226 -0
- package/test/ask-user-gate.test.js +231 -0
- package/test/background.test.js +414 -0
- package/test/chat-history-nocolor.test.js +155 -0
- package/test/chat-relogin.test.js +207 -0
- package/test/chat.test.js +114 -0
- package/test/checkpoints-agent.test.js +181 -0
- package/test/checkpoints.test.js +650 -0
- package/test/command-registry.test.js +160 -0
- package/test/compact.test.js +116 -0
- package/test/completion-lazy.test.js +52 -0
- package/test/config-merge.test.js +324 -0
- package/test/config-quarantine.test.js +128 -0
- package/test/config-write-guard-allow-anywhere.test.js +56 -0
- package/test/config-write-guard-skip.test.js +46 -0
- package/test/config-write-guard.test.js +153 -0
- package/test/context-split.test.js +215 -0
- package/test/cost-doctor.test.js +142 -0
- package/test/custom-commands-chat.test.js +106 -0
- package/test/custom-commands.test.js +230 -0
- package/test/defer-detail-band.test.js +403 -0
- package/test/deny-windows.test.js +120 -0
- package/test/deny.test.js +83 -0
- package/test/detail-band-tab-flatten.test.js +242 -0
- package/test/download-allow-anywhere.test.js +66 -0
- package/test/download-confine.test.js +153 -0
- package/test/exec-diff.test.js +268 -0
- package/test/executors.test.js +599 -0
- package/test/extract-tool-calls.test.js +349 -0
- package/test/fetch-url-validation.test.js +219 -0
- package/test/file-activity.test.js +522 -0
- package/test/fixtures/tool-calls.js +57 -0
- package/test/fixtures/web-page.js +91 -0
- package/test/git-tools.test.js +384 -0
- package/test/grep-glob-serialize.test.js +242 -0
- package/test/grep-glob.test.js +268 -0
- package/test/grep-path-target.test.js +227 -0
- package/test/harness/README.md +57 -0
- package/test/harness/chat-harness.js +143 -0
- package/test/harness/memwarn-headless-child.js +65 -0
- package/test/harness/mock-llm.js +120 -0
- package/test/harness/mock-mcp-server.js +142 -0
- package/test/harness/sse-server.js +69 -0
- package/test/headless.test.js +348 -0
- package/test/history-utils.test.js +88 -0
- package/test/hooks-agent.test.js +238 -0
- package/test/hooks-verify-sandbox.test.js +232 -0
- package/test/hooks.test.js +216 -0
- package/test/http-get-user-agent.test.js +142 -0
- package/test/images-api.test.js +208 -0
- package/test/images.test.js +238 -0
- package/test/input-field-ctrl-o.test.js +37 -0
- package/test/live-height-physical.test.js +281 -0
- package/test/max-iterations.test.js +218 -0
- package/test/mcp-boundary.test.js +57 -0
- package/test/mcp-client.test.js +267 -0
- package/test/mcp-oauth.test.js +86 -0
- package/test/md-stream.test.js +183 -0
- package/test/memory-truncation-warning.test.js +222 -0
- package/test/memory.test.js +198 -0
- package/test/native-dispatch.test.js +409 -0
- package/test/native-live-narration.test.js +254 -0
- package/test/output-chokepoint.test.js +188 -0
- package/test/output-heredoc-leak.test.js +195 -0
- package/test/output-preview.test.js +245 -0
- package/test/path-guards.test.js +134 -0
- package/test/payload.test.js +99 -0
- package/test/permission-rules-agent.test.js +210 -0
- package/test/permission-rules.test.js +297 -0
- package/test/permissions.test.js +362 -0
- package/test/plan-mode.test.js +167 -0
- package/test/read-paginate.test.js +275 -0
- package/test/readonly-tools.test.js +177 -0
- package/test/render-operation.test.js +317 -0
- package/test/replay-descriptor-xml.test.js +216 -0
- package/test/replay-descriptor.test.js +189 -0
- package/test/replay-web-aggregate.test.js +291 -0
- package/test/replay-web-persist.test.js +241 -0
- package/test/result-cap.test.js +233 -0
- package/test/running-glyph-anim.test.js +111 -0
- package/test/sandbox-agent.test.js +147 -0
- package/test/sandbox-integration.test.js +216 -0
- package/test/sandbox.test.js +408 -0
- package/test/sdk.test.js +234 -0
- package/test/shell-output-cap.test.js +181 -0
- package/test/skills-chat.test.js +110 -0
- package/test/skills.test.js +295 -0
- package/test/smoke.test.js +68 -0
- package/test/status-bar-driver.test.js +93 -0
- package/test/status-bar-pause.test.js +164 -0
- package/test/status-bar-resync.test.js +188 -0
- package/test/stream-parser.test.js +171 -0
- package/test/subagents-agent.test.js +178 -0
- package/test/subagents.test.js +222 -0
- package/test/theme-palette.test.js +166 -0
- package/test/tool-registry.test.js +85 -0
- package/test/trim-budget.test.js +101 -0
- package/test/truncate-visible.test.js +78 -0
- package/test/verify-agent.test.js +317 -0
- package/test/verify.test.js +141 -0
- package/test/view-image.test.js +199 -0
- package/test/web-activity-ordering.test.js +203 -0
- package/test/web-activity.test.js +207 -0
- package/test/web-data-extraction-guidance.test.js +71 -0
- package/test/web-extract.test.js +185 -0
- package/test/web-fetch-agent.test.js +291 -0
- package/test/web-fetch-mode.test.js +193 -0
- package/test/web-search.test.js +380 -0
- package/lib/commands.js +0 -1438
- package/path +0 -1
package/lib/memory.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Project memory — AGENTS.md / CLAUDE.md hierarchy (Task 2.3)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
//
|
|
7
|
+
// On session start the agent loads project-local instruction files and appends
|
|
8
|
+
// them to the system prompt, marked as distinct, trusted project guidance. The
|
|
9
|
+
// hierarchy, concatenated in this order (all that exist):
|
|
10
|
+
//
|
|
11
|
+
// 1. global ~/.semalt-ai/AGENTS.md
|
|
12
|
+
// 2. project root <repo root>/AGENTS.md (repo root = nearest .git ancestor)
|
|
13
|
+
// 3. cwd <cwd>/AGENTS.md (only when CWD is nested below root)
|
|
14
|
+
//
|
|
15
|
+
// At each level CLAUDE.md is an alias for AGENTS.md: AGENTS.md is preferred when
|
|
16
|
+
// both exist, and the choice (plus the ignored CLAUDE.md) is reported. The total
|
|
17
|
+
// size is bounded — oversized memory is truncated with a visible notice rather
|
|
18
|
+
// than blowing the context. API-key/secret files are never involved here; these
|
|
19
|
+
// are plain project docs.
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
// Keep memory from dominating the context. 32 KB is comfortably above a typical
|
|
26
|
+
// AGENTS.md yet far below any model window.
|
|
27
|
+
const DEFAULT_MEMORY_MAX_BYTES = 32 * 1024;
|
|
28
|
+
|
|
29
|
+
function _isFile(p) {
|
|
30
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Nearest ancestor (inclusive) containing a .git entry, or null.
|
|
34
|
+
function findRepoRoot(startDir) {
|
|
35
|
+
let dir = path.resolve(startDir);
|
|
36
|
+
while (true) {
|
|
37
|
+
try { if (fs.existsSync(path.join(dir, '.git'))) return dir; } catch {}
|
|
38
|
+
const parent = path.dirname(dir);
|
|
39
|
+
if (parent === dir) return null;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Pick the memory file for a directory: AGENTS.md preferred, CLAUDE.md alias.
|
|
45
|
+
// Returns { path, name, alsoPresent } or null. `alsoPresent` is true when both
|
|
46
|
+
// files exist (CLAUDE.md was present but ignored in favor of AGENTS.md).
|
|
47
|
+
function _pickMemoryFile(dir) {
|
|
48
|
+
const agents = path.join(dir, 'AGENTS.md');
|
|
49
|
+
const claude = path.join(dir, 'CLAUDE.md');
|
|
50
|
+
const hasAgents = _isFile(agents);
|
|
51
|
+
const hasClaude = _isFile(claude);
|
|
52
|
+
if (hasAgents) return { path: agents, name: 'AGENTS.md', alsoPresent: hasClaude };
|
|
53
|
+
if (hasClaude) return { path: claude, name: 'CLAUDE.md', alsoPresent: false };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Resolve the ordered set of memory files for a (cwd, home), de-duplicated by
|
|
58
|
+
// resolved path so a level that coincides with another is not loaded twice.
|
|
59
|
+
function discoverMemoryFiles(cwd = process.cwd(), home = os.homedir()) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const add = (dir, source) => {
|
|
63
|
+
const picked = _pickMemoryFile(dir);
|
|
64
|
+
if (!picked) return;
|
|
65
|
+
const real = path.resolve(picked.path);
|
|
66
|
+
if (seen.has(real)) return;
|
|
67
|
+
seen.add(real);
|
|
68
|
+
out.push({ ...picked, source });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
add(path.join(home, '.semalt-ai'), 'global');
|
|
72
|
+
const repoRoot = findRepoRoot(cwd);
|
|
73
|
+
const projectRoot = repoRoot || cwd;
|
|
74
|
+
add(projectRoot, 'project-root');
|
|
75
|
+
if (path.resolve(cwd) !== path.resolve(projectRoot)) add(cwd, 'cwd');
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Per-file truncation accounting. The block joins all loaded files (with a
|
|
80
|
+
// `# path (source)\n` header each, separated by '\n\n') and then slices the
|
|
81
|
+
// whole body at the cap, so a file may be fully kept, partially cut, or wholly
|
|
82
|
+
// dropped depending on where it falls. This mirrors the exact char-based slice
|
|
83
|
+
// in _buildBlock (NOT changed — see the comment there) to report which files
|
|
84
|
+
// lost content and by how much. Returns one entry per file that was truncated:
|
|
85
|
+
// { path, source, originalBytes, loadedBytes }
|
|
86
|
+
function _truncatedFileDetails(loadedFiles, cutChars) {
|
|
87
|
+
const out = [];
|
|
88
|
+
let offset = 0; // char offset of the current section within the joined body
|
|
89
|
+
for (let i = 0; i < loadedFiles.length; i++) {
|
|
90
|
+
const f = loadedFiles[i];
|
|
91
|
+
if (i > 0) offset += 2; // the '\n\n' separator between sections
|
|
92
|
+
const header = `# ${f.path} (${f.source})\n`;
|
|
93
|
+
const contentStart = offset + header.length;
|
|
94
|
+
const survivedChars = Math.max(0, Math.min(f.content.length, cutChars - contentStart));
|
|
95
|
+
const loadedBytes = survivedChars >= f.content.length
|
|
96
|
+
? f.bytes
|
|
97
|
+
: Buffer.byteLength(f.content.slice(0, survivedChars), 'utf8');
|
|
98
|
+
if (loadedBytes < f.bytes) {
|
|
99
|
+
out.push({ path: f.path, source: f.source, originalBytes: f.bytes, loadedBytes });
|
|
100
|
+
}
|
|
101
|
+
offset = contentStart + f.content.length;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _buildBlock(loadedFiles, maxBytes) {
|
|
107
|
+
if (!loadedFiles.length) return { block: '', truncated: false, truncatedFiles: [] };
|
|
108
|
+
const sections = loadedFiles.map((f) => `# ${f.path} (${f.source})\n${f.content}`);
|
|
109
|
+
let body = sections.join('\n\n');
|
|
110
|
+
let truncated = false;
|
|
111
|
+
let truncatedFiles = [];
|
|
112
|
+
if (Buffer.byteLength(body, 'utf8') > maxBytes) {
|
|
113
|
+
// NOTE: char-index slice against a byte cap — a pre-existing approximation
|
|
114
|
+
// (exact for ASCII). Do not change the loading logic; the warning path
|
|
115
|
+
// (Task: fail-loud memory truncation) only surfaces the existing cut.
|
|
116
|
+
truncatedFiles = _truncatedFileDetails(loadedFiles, maxBytes);
|
|
117
|
+
body = body.slice(0, maxBytes);
|
|
118
|
+
truncated = true;
|
|
119
|
+
}
|
|
120
|
+
let block = '\n\n<<<PROJECT_MEMORY>>>\n'
|
|
121
|
+
+ 'The following are project-specific instructions loaded from AGENTS.md/CLAUDE.md '
|
|
122
|
+
+ 'files (the cross-tool project-memory standard). Treat them as authoritative user '
|
|
123
|
+
+ 'guidance for this project, distinct from your base instructions above. This is '
|
|
124
|
+
+ 'trusted project context, not untrusted external content.\n\n'
|
|
125
|
+
+ body;
|
|
126
|
+
if (truncated) {
|
|
127
|
+
block += `\n\n[project memory truncated to ${maxBytes} bytes — some content omitted. `
|
|
128
|
+
+ 'Trim your AGENTS.md/CLAUDE.md files if important guidance is being cut.]';
|
|
129
|
+
}
|
|
130
|
+
block += '\n<<<END_PROJECT_MEMORY>>>';
|
|
131
|
+
return { block, truncated, truncatedFiles };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Load project memory for the current (or supplied) cwd/home. Returns:
|
|
135
|
+
// { block, files, truncated }
|
|
136
|
+
// where `block` is '' when no memory files exist (so the system prompt is
|
|
137
|
+
// byte-for-byte unchanged), and `files` is the metadata list (no content) used
|
|
138
|
+
// by the /memory command.
|
|
139
|
+
function loadProjectMemory(opts = {}) {
|
|
140
|
+
const cwd = opts.cwd || process.cwd();
|
|
141
|
+
const home = opts.home || os.homedir();
|
|
142
|
+
const maxBytes = opts.maxBytes || DEFAULT_MEMORY_MAX_BYTES;
|
|
143
|
+
const discovered = discoverMemoryFiles(cwd, home);
|
|
144
|
+
const loaded = [];
|
|
145
|
+
for (const d of discovered) {
|
|
146
|
+
let content;
|
|
147
|
+
try { content = fs.readFileSync(d.path, 'utf8'); } catch { continue; }
|
|
148
|
+
loaded.push({ ...d, content, bytes: Buffer.byteLength(content, 'utf8') });
|
|
149
|
+
}
|
|
150
|
+
const { block, truncated, truncatedFiles } = _buildBlock(loaded, maxBytes);
|
|
151
|
+
const files = loaded.map(({ content, ...meta }) => meta); // strip content
|
|
152
|
+
return { block, files, truncated, truncatedFiles };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Human-readable size, e.g. 145408 → "142 KB", 800 → "800 B".
|
|
156
|
+
function _fmtBytes(bytes) {
|
|
157
|
+
return bytes >= 1024 ? `${Math.round(bytes / 1024)} KB` : `${bytes} B`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// One-time, user-facing truncation warnings (fail-loud — the project never
|
|
161
|
+
// silently drops loaded memory). Pure: maps the `truncatedFiles` detail from
|
|
162
|
+
// loadProjectMemory() to actionable strings (path + loaded/original size +
|
|
163
|
+
// dropped %). Returns [] when nothing was truncated, so callers warn only when
|
|
164
|
+
// content was actually dropped. This text is for the USER channel (stderr /
|
|
165
|
+
// chat system line / SDK 'warning' event) — never the model/system prompt.
|
|
166
|
+
function memoryTruncationWarnings(result) {
|
|
167
|
+
const files = (result && result.truncatedFiles) || [];
|
|
168
|
+
return files.map((t) => {
|
|
169
|
+
const dropped = Math.max(0, t.originalBytes - t.loadedBytes);
|
|
170
|
+
const pct = t.originalBytes > 0 ? Math.round((dropped / t.originalBytes) * 100) : 0;
|
|
171
|
+
return `⚠ Memory file ${t.path} truncated: loaded ${_fmtBytes(t.loadedBytes)} of `
|
|
172
|
+
+ `${_fmtBytes(t.originalBytes)} (${pct}% dropped). `
|
|
173
|
+
+ 'Consider trimming it to the most relevant guidance.';
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Human-readable status lines for the /memory command: which files loaded, their
|
|
178
|
+
// resolved paths, the alias choice, truncation, and where to edit.
|
|
179
|
+
function memoryStatusLines(result) {
|
|
180
|
+
const lines = [];
|
|
181
|
+
if (!result.files.length) {
|
|
182
|
+
lines.push('No project memory files found.');
|
|
183
|
+
lines.push('Create an AGENTS.md (or CLAUDE.md) in your repo root to add project instructions.');
|
|
184
|
+
return lines;
|
|
185
|
+
}
|
|
186
|
+
lines.push(`Loaded ${result.files.length} project memory file(s):`);
|
|
187
|
+
for (const f of result.files) {
|
|
188
|
+
let line = ` • ${f.path} [${f.source}]`;
|
|
189
|
+
if (f.alsoPresent) line += ' (chose AGENTS.md; CLAUDE.md also present, ignored)';
|
|
190
|
+
lines.push(line);
|
|
191
|
+
}
|
|
192
|
+
if (result.truncated) {
|
|
193
|
+
lines.push('⚠ Project memory was truncated (too large). Trim your memory files.');
|
|
194
|
+
}
|
|
195
|
+
const editTarget = result.files.find((f) => f.source === 'cwd')
|
|
196
|
+
|| result.files.find((f) => f.source === 'project-root')
|
|
197
|
+
|| result.files[0];
|
|
198
|
+
lines.push(`Edit project memory: ${editTarget.path}`);
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
DEFAULT_MEMORY_MAX_BYTES,
|
|
204
|
+
findRepoRoot,
|
|
205
|
+
discoverMemoryFiles,
|
|
206
|
+
loadProjectMemory,
|
|
207
|
+
memoryStatusLines,
|
|
208
|
+
memoryTruncationWarnings,
|
|
209
|
+
};
|
package/lib/metrics.js
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const { THEME } = require('./ui');
|
|
4
4
|
|
|
5
|
+
// Compact token count for the estimated-split summary row (Variant B): the
|
|
6
|
+
// base/working estimates are abbreviated (12k, 5.6k) so the row fits the fixed
|
|
7
|
+
// summary-box width. They're estimates, so sub-thousand precision is noise.
|
|
8
|
+
function abbrevTokens(n) {
|
|
9
|
+
const v = Math.max(0, Math.round(Number(n) || 0));
|
|
10
|
+
if (v < 1000) return String(v);
|
|
11
|
+
const k = v / 1000;
|
|
12
|
+
return (k < 10 ? k.toFixed(1) : String(Math.round(k))) + 'k';
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
class Metrics {
|
|
6
16
|
constructor(modelTokenLimit = null) {
|
|
7
17
|
this.sessionStart = Date.now();
|
|
@@ -10,16 +20,21 @@ class Metrics {
|
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
startTurn() {
|
|
13
|
-
this.turns.push({ start: Date.now(), promptTokens: 0, completionTokens: 0 });
|
|
23
|
+
this.turns.push({ start: Date.now(), promptTokens: 0, completionTokens: 0, baseEst: 0, workingEst: 0 });
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
endTurn(usage, model) {
|
|
26
|
+
endTurn(usage, model, contextEstimate) {
|
|
17
27
|
const last = this.turns[this.turns.length - 1];
|
|
18
28
|
if (!last) return;
|
|
19
29
|
last.end = Date.now();
|
|
20
30
|
last.promptTokens = (usage && usage.prompt_tokens) || 0;
|
|
21
31
|
last.completionTokens = (usage && usage.completion_tokens) || 0;
|
|
22
32
|
last.model = model;
|
|
33
|
+
// Estimated base/working split (Variant B, display-only). The real
|
|
34
|
+
// promptTokens above stays the truth anchor; these are char/4 estimates of
|
|
35
|
+
// the same prompt's parts, recomputed per request by the api client.
|
|
36
|
+
last.baseEst = (contextEstimate && contextEstimate.base) || 0;
|
|
37
|
+
last.workingEst = (contextEstimate && contextEstimate.working) || 0;
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
totalTokens() {
|
|
@@ -31,6 +46,19 @@ class Metrics {
|
|
|
31
46
|
return this.turns[this.turns.length - 1].promptTokens;
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
// Estimated split of the current (last turn's) context — display-only
|
|
50
|
+
// (Variant B). Both are char/4 estimates that sum consistently; the real
|
|
51
|
+
// contextTokens() above is the measured anchor shown alongside them.
|
|
52
|
+
contextBaseEst() {
|
|
53
|
+
if (!this.turns.length) return 0;
|
|
54
|
+
return this.turns[this.turns.length - 1].baseEst || 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
contextWorkingEst() {
|
|
58
|
+
if (!this.turns.length) return 0;
|
|
59
|
+
return this.turns[this.turns.length - 1].workingEst || 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
34
62
|
tokenLimitStatus() {
|
|
35
63
|
const used = this.contextTokens();
|
|
36
64
|
if (this.modelTokenLimit == null) {
|
|
@@ -94,6 +122,13 @@ class Metrics {
|
|
|
94
122
|
lines.push(row(` Context used: ${this.contextTokens()}`));
|
|
95
123
|
lines.push(row(` Token limit: ${status.used}/${status.limit} (${status.pct}%)`));
|
|
96
124
|
}
|
|
125
|
+
// Estimated breakdown of the measured context above (Variant B). The ~
|
|
126
|
+
// marks these as estimates; the measured total is the line above (no ~).
|
|
127
|
+
const baseEst = this.contextBaseEst();
|
|
128
|
+
const workingEst = this.contextWorkingEst();
|
|
129
|
+
if (baseEst > 0 || workingEst > 0) {
|
|
130
|
+
lines.push(row(` Est. split: ~${abbrevTokens(workingEst)} work · ~${abbrevTokens(baseEst)} base`));
|
|
131
|
+
}
|
|
97
132
|
}
|
|
98
133
|
|
|
99
134
|
lines.push(row(` Duration: ${durationStr}`));
|
package/lib/payload.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Optional chat/completions payload augmentations (Task 2.7).
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
//
|
|
7
|
+
// Pure functions, gated by config + model support, applied to the request body
|
|
8
|
+
// just before it is serialized. Kept separate from api.js so the exact field
|
|
9
|
+
// presence/absence is unit-testable without a live endpoint.
|
|
10
|
+
|
|
11
|
+
// Prompt caching: mark the STABLE PREFIX — the last system message and the end
|
|
12
|
+
// of the tools array — with Anthropic-style cache_control:{type:'ephemeral'} so
|
|
13
|
+
// providers that honor it reuse the prefix across turns. Only mutates when
|
|
14
|
+
// enabled; absent otherwise. Gated upstream by config.prompt_caching, so it is
|
|
15
|
+
// never sent to endpoints the user hasn't opted in for.
|
|
16
|
+
function applyPromptCaching(payload, enabled) {
|
|
17
|
+
if (!enabled || !payload) return payload;
|
|
18
|
+
if (Array.isArray(payload.messages)) {
|
|
19
|
+
for (let i = payload.messages.length - 1; i >= 0; i--) {
|
|
20
|
+
const m = payload.messages[i];
|
|
21
|
+
if (m && m.role === 'system') {
|
|
22
|
+
payload.messages[i] = { ...m, cache_control: { type: 'ephemeral' } };
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(payload.tools) && payload.tools.length) {
|
|
28
|
+
const last = payload.tools.length - 1;
|
|
29
|
+
payload.tools[last] = { ...payload.tools[last], cache_control: { type: 'ephemeral' } };
|
|
30
|
+
}
|
|
31
|
+
return payload;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Heuristic for OpenAI-style `reasoning_effort` support: reasoning model
|
|
35
|
+
// families (o1–o4, gpt-5, *-reasoning/-thinking, deepseek-r1, qwq).
|
|
36
|
+
function supportsReasoningEffort(model) {
|
|
37
|
+
if (typeof model !== 'string' || !model) return false;
|
|
38
|
+
return /(^|[/\-])o[1-4]([-/]|$|mini|preview)|gpt-5|reason|deepseek-r1|(^|[/\-])r1([-/]|$)|thinking|qwq/i.test(model);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const VALID_EFFORTS = new Set(['minimal', 'low', 'medium', 'high']);
|
|
42
|
+
|
|
43
|
+
// Add reasoning_effort when configured and the model supports it (or support is
|
|
44
|
+
// forced for a model the heuristic misses). No-op otherwise.
|
|
45
|
+
function applyReasoningEffort(payload, effort, model, { force = false } = {}) {
|
|
46
|
+
if (!payload || !effort) return payload;
|
|
47
|
+
const e = String(effort).toLowerCase();
|
|
48
|
+
if (!VALID_EFFORTS.has(e)) return payload;
|
|
49
|
+
if (!force && !supportsReasoningEffort(model)) return payload;
|
|
50
|
+
payload.reasoning_effort = e;
|
|
51
|
+
return payload;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { applyPromptCaching, supportsReasoningEffort, applyReasoningEffort, VALID_EFFORTS };
|