@aexol/spectral 0.7.1 → 0.7.5
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/CHANGELOG.md +5 -0
- package/dist/agent/agents.js +1 -1
- package/dist/agent/index.js +199 -184
- package/dist/commands/serve.js +0 -3
- package/dist/designer/data/systems/renault/DESIGN.md +1 -1
- package/dist/designer/philosophies.js +668 -0
- package/dist/mcp/sampling-handler.js +1 -1
- package/dist/memory/commands/status.js +1 -1
- package/dist/memory/compaction.js +2 -2
- package/dist/memory/config.js +1 -1
- package/dist/memory/debug-log.js +1 -1
- package/dist/memory/hooks/compaction-hook.js +29 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/observer.js +2 -2
- package/dist/memory/project-observations-store.js +14 -0
- package/dist/memory/tokens.js +1 -1
- package/dist/memory/tools/read-project-observations.js +82 -0
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/pi/agent-core/agent-loop.js +501 -0
- package/dist/pi/agent-core/agent.js +401 -0
- package/dist/pi/agent-core/harness/agent-harness.js +899 -0
- package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
- package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
- package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
- package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
- package/dist/pi/agent-core/harness/messages.js +101 -0
- package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
- package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
- package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
- package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
- package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
- package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
- package/dist/pi/agent-core/harness/session/session.js +196 -0
- package/dist/pi/agent-core/harness/session/uuid.js +49 -0
- package/dist/pi/agent-core/harness/skills.js +310 -0
- package/dist/pi/agent-core/harness/system-prompt.js +29 -0
- package/dist/pi/agent-core/harness/types.js +93 -0
- package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
- package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
- package/dist/pi/agent-core/index.js +24 -0
- package/dist/pi/agent-core/node.js +2 -0
- package/dist/pi/agent-core/proxy.js +277 -0
- package/dist/pi/agent-core/types.js +1 -0
- package/dist/pi/ai/api-registry.js +43 -0
- package/dist/pi/ai/cli.js +120 -0
- package/dist/pi/ai/env-api-keys.js +169 -0
- package/dist/pi/ai/image-models.generated.js +441 -0
- package/dist/pi/ai/image-models.js +22 -0
- package/dist/pi/ai/images-api-registry.js +21 -0
- package/dist/pi/ai/images.js +13 -0
- package/dist/pi/ai/index.js +18 -0
- package/dist/pi/ai/models.generated.js +16220 -0
- package/dist/pi/ai/models.js +70 -0
- package/dist/pi/ai/oauth.js +1 -0
- package/dist/pi/ai/providers/anthropic.js +945 -0
- package/dist/pi/ai/providers/faux.js +367 -0
- package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
- package/dist/pi/ai/providers/openai-completions.js +945 -0
- package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
- package/dist/pi/ai/providers/register-builtins.js +97 -0
- package/dist/pi/ai/providers/simple-options.js +40 -0
- package/dist/pi/ai/providers/transform-messages.js +183 -0
- package/dist/pi/ai/session-resources.js +21 -0
- package/dist/pi/ai/stream.js +26 -0
- package/dist/pi/ai/types.js +1 -0
- package/dist/pi/ai/utils/diagnostics.js +24 -0
- package/dist/pi/ai/utils/event-stream.js +80 -0
- package/dist/pi/ai/utils/hash.js +13 -0
- package/dist/pi/ai/utils/headers.js +7 -0
- package/dist/pi/ai/utils/json-parse.js +112 -0
- package/dist/pi/ai/utils/node-http-proxy.js +96 -0
- package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
- package/dist/pi/ai/utils/oauth/device-code.js +54 -0
- package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
- package/dist/pi/ai/utils/oauth/index.js +121 -0
- package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
- package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
- package/dist/pi/ai/utils/oauth/pkce.js +30 -0
- package/dist/pi/ai/utils/oauth/types.js +1 -0
- package/dist/pi/ai/utils/overflow.js +150 -0
- package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
- package/dist/pi/ai/utils/typebox-helpers.js +20 -0
- package/dist/pi/ai/utils/validation.js +280 -0
- package/dist/pi/coding-agent/bun/cli.js +7 -0
- package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
- package/dist/pi/coding-agent/cli/args.js +340 -0
- package/dist/pi/coding-agent/cli/file-processor.js +82 -0
- package/dist/pi/coding-agent/cli/initial-message.js +21 -0
- package/dist/pi/coding-agent/cli.js +17 -0
- package/dist/pi/coding-agent/config.js +414 -0
- package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
- package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
- package/dist/pi/coding-agent/core/agent-session.js +2498 -0
- package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
- package/dist/pi/coding-agent/core/auth-storage.js +441 -0
- package/dist/pi/coding-agent/core/bash-executor.js +110 -0
- package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
- package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
- package/dist/pi/coding-agent/core/compaction/index.js +6 -0
- package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
- package/dist/pi/coding-agent/core/defaults.js +1 -0
- package/dist/pi/coding-agent/core/diagnostics.js +1 -0
- package/dist/pi/coding-agent/core/event-bus.js +24 -0
- package/dist/pi/coding-agent/core/exec.js +74 -0
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
- package/dist/pi/coding-agent/core/export-html/index.js +225 -0
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
- package/dist/pi/coding-agent/core/extensions/index.js +8 -0
- package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
- package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
- package/dist/pi/coding-agent/core/extensions/types.js +44 -0
- package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
- package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
- package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
- package/dist/pi/coding-agent/core/index.js +11 -0
- package/dist/pi/coding-agent/core/keybindings.js +294 -0
- package/dist/pi/coding-agent/core/messages.js +122 -0
- package/dist/pi/coding-agent/core/model-registry.js +728 -0
- package/dist/pi/coding-agent/core/model-resolver.js +494 -0
- package/dist/pi/coding-agent/core/output-guard.js +58 -0
- package/dist/pi/coding-agent/core/package-manager.js +2020 -0
- package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
- package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
- package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
- package/dist/pi/coding-agent/core/resource-loader.js +733 -0
- package/dist/pi/coding-agent/core/sdk.js +282 -0
- package/dist/pi/coding-agent/core/session-cwd.js +37 -0
- package/dist/pi/coding-agent/core/session-manager.js +1146 -0
- package/dist/pi/coding-agent/core/settings-manager.js +794 -0
- package/dist/pi/coding-agent/core/skills.js +386 -0
- package/dist/pi/coding-agent/core/slash-commands.js +24 -0
- package/dist/pi/coding-agent/core/source-info.js +18 -0
- package/dist/pi/coding-agent/core/system-prompt.js +122 -0
- package/dist/pi/coding-agent/core/telemetry.js +8 -0
- package/dist/pi/coding-agent/core/timings.js +30 -0
- package/dist/pi/coding-agent/core/tools/bash.js +341 -0
- package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
- package/dist/pi/coding-agent/core/tools/edit.js +324 -0
- package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
- package/dist/pi/coding-agent/core/tools/find.js +297 -0
- package/dist/pi/coding-agent/core/tools/grep.js +303 -0
- package/dist/pi/coding-agent/core/tools/index.js +111 -0
- package/dist/pi/coding-agent/core/tools/ls.js +168 -0
- package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
- package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
- package/dist/pi/coding-agent/core/tools/read.js +288 -0
- package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
- package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
- package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
- package/dist/pi/coding-agent/core/tools/write.js +212 -0
- package/dist/pi/coding-agent/index.js +41 -0
- package/dist/pi/coding-agent/main.js +5 -0
- package/dist/pi/coding-agent/migrations.js +280 -0
- package/dist/pi/coding-agent/modes/index.js +7 -0
- package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
- package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
- package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
- package/dist/pi/coding-agent/modes/print-mode.js +130 -0
- package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
- package/dist/pi/coding-agent/utils/ansi.js +51 -0
- package/dist/pi/coding-agent/utils/changelog.js +86 -0
- package/dist/pi/coding-agent/utils/child-process.js +87 -0
- package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
- package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
- package/dist/pi/coding-agent/utils/clipboard.js +116 -0
- package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
- package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
- package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
- package/dist/pi/coding-agent/utils/git.js +162 -0
- package/dist/pi/coding-agent/utils/html.js +39 -0
- package/dist/pi/coding-agent/utils/image-convert.js +38 -0
- package/dist/pi/coding-agent/utils/image-resize.js +136 -0
- package/dist/pi/coding-agent/utils/mime.js +68 -0
- package/dist/pi/coding-agent/utils/paths.js +91 -0
- package/dist/pi/coding-agent/utils/photon.js +120 -0
- package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
- package/dist/pi/coding-agent/utils/shell.js +194 -0
- package/dist/pi/coding-agent/utils/sleep.js +16 -0
- package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
- package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
- package/dist/pi/coding-agent/utils/version-check.js +81 -0
- package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
- package/dist/pi/tui/autocomplete.js +631 -0
- package/dist/pi/tui/components/box.js +103 -0
- package/dist/pi/tui/components/cancellable-loader.js +34 -0
- package/dist/pi/tui/components/editor.js +1915 -0
- package/dist/pi/tui/components/image.js +88 -0
- package/dist/pi/tui/components/input.js +425 -0
- package/dist/pi/tui/components/loader.js +68 -0
- package/dist/pi/tui/components/markdown.js +633 -0
- package/dist/pi/tui/components/select-list.js +158 -0
- package/dist/pi/tui/components/settings-list.js +184 -0
- package/dist/pi/tui/components/spacer.js +22 -0
- package/dist/pi/tui/components/text.js +88 -0
- package/dist/pi/tui/components/truncated-text.js +50 -0
- package/dist/pi/tui/editor-component.js +1 -0
- package/dist/pi/tui/fuzzy.js +109 -0
- package/dist/pi/tui/index.js +31 -0
- package/dist/pi/tui/keybindings.js +173 -0
- package/dist/pi/tui/keys.js +1172 -0
- package/dist/pi/tui/kill-ring.js +43 -0
- package/dist/pi/tui/stdin-buffer.js +360 -0
- package/dist/pi/tui/terminal-image.js +335 -0
- package/dist/pi/tui/terminal.js +324 -0
- package/dist/pi/tui/tui.js +1076 -0
- package/dist/pi/tui/undo-stack.js +24 -0
- package/dist/pi/tui/utils.js +1016 -0
- package/dist/relay/dispatcher.js +30 -0
- package/dist/server/handlers/queue.js +52 -0
- package/dist/server/pi-bridge.js +9 -1
- package/dist/server/session-stream.js +76 -111
- package/dist/server/storage.js +154 -2
- package/dist/server/title-generator.js +14 -153
- package/package.json +24 -6
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared truncation utilities for tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Truncation is based on two independent limits - whichever is hit first wins:
|
|
5
|
+
* - Line limit (default: 2000 lines)
|
|
6
|
+
* - Byte limit (default: 50KB)
|
|
7
|
+
*
|
|
8
|
+
* Never returns partial lines (except bash tail truncation edge case).
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_MAX_LINES = 2000;
|
|
11
|
+
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
12
|
+
export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
|
|
13
|
+
const runtimeBuffer = globalThis.Buffer;
|
|
14
|
+
const nonAsciiPattern = /[^\x00-\x7f]/;
|
|
15
|
+
function utf8ByteLength(content) {
|
|
16
|
+
if (runtimeBuffer)
|
|
17
|
+
return runtimeBuffer.byteLength(content, "utf8");
|
|
18
|
+
const firstNonAscii = content.search(nonAsciiPattern);
|
|
19
|
+
if (firstNonAscii === -1)
|
|
20
|
+
return content.length;
|
|
21
|
+
let bytes = firstNonAscii;
|
|
22
|
+
for (let i = firstNonAscii; i < content.length; i++) {
|
|
23
|
+
const code = content.charCodeAt(i);
|
|
24
|
+
if (code <= 0x7f) {
|
|
25
|
+
bytes += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (code <= 0x7ff) {
|
|
28
|
+
bytes += 2;
|
|
29
|
+
}
|
|
30
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {
|
|
31
|
+
const next = content.charCodeAt(i + 1);
|
|
32
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
33
|
+
bytes += 4;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
bytes += 3;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
bytes += 3;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
function replaceUnpairedSurrogates(content) {
|
|
47
|
+
let output = "";
|
|
48
|
+
for (let i = 0; i < content.length; i++) {
|
|
49
|
+
const code = content.charCodeAt(i);
|
|
50
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
51
|
+
if (i + 1 < content.length) {
|
|
52
|
+
const next = content.charCodeAt(i + 1);
|
|
53
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
54
|
+
output += content[i] + content[i + 1];
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
output += "�";
|
|
60
|
+
}
|
|
61
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
62
|
+
output += "�";
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
output += content[i];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Format bytes as human-readable size.
|
|
72
|
+
*/
|
|
73
|
+
export function formatSize(bytes) {
|
|
74
|
+
if (bytes < 1024) {
|
|
75
|
+
return `${bytes}B`;
|
|
76
|
+
}
|
|
77
|
+
else if (bytes < 1024 * 1024) {
|
|
78
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Truncate content from the head (keep first N lines/bytes).
|
|
86
|
+
* Suitable for file reads where you want to see the beginning.
|
|
87
|
+
*
|
|
88
|
+
* Never returns partial lines. If first line exceeds byte limit,
|
|
89
|
+
* returns empty content with firstLineExceedsLimit=true.
|
|
90
|
+
*/
|
|
91
|
+
export function truncateHead(content, options = {}) {
|
|
92
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
93
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
94
|
+
const totalBytes = utf8ByteLength(content);
|
|
95
|
+
const lines = content.split("\n");
|
|
96
|
+
const totalLines = lines.length;
|
|
97
|
+
// Check if no truncation needed
|
|
98
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
99
|
+
return {
|
|
100
|
+
content,
|
|
101
|
+
truncated: false,
|
|
102
|
+
truncatedBy: null,
|
|
103
|
+
totalLines,
|
|
104
|
+
totalBytes,
|
|
105
|
+
outputLines: totalLines,
|
|
106
|
+
outputBytes: totalBytes,
|
|
107
|
+
lastLinePartial: false,
|
|
108
|
+
firstLineExceedsLimit: false,
|
|
109
|
+
maxLines,
|
|
110
|
+
maxBytes,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Check if first line alone exceeds byte limit
|
|
114
|
+
const firstLineBytes = utf8ByteLength(lines[0]);
|
|
115
|
+
if (firstLineBytes > maxBytes) {
|
|
116
|
+
return {
|
|
117
|
+
content: "",
|
|
118
|
+
truncated: true,
|
|
119
|
+
truncatedBy: "bytes",
|
|
120
|
+
totalLines,
|
|
121
|
+
totalBytes,
|
|
122
|
+
outputLines: 0,
|
|
123
|
+
outputBytes: 0,
|
|
124
|
+
lastLinePartial: false,
|
|
125
|
+
firstLineExceedsLimit: true,
|
|
126
|
+
maxLines,
|
|
127
|
+
maxBytes,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Collect complete lines that fit
|
|
131
|
+
const outputLinesArr = [];
|
|
132
|
+
let outputBytesCount = 0;
|
|
133
|
+
let truncatedBy = "lines";
|
|
134
|
+
for (let i = 0; i < lines.length && i < maxLines; i++) {
|
|
135
|
+
const line = lines[i];
|
|
136
|
+
const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline
|
|
137
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
138
|
+
truncatedBy = "bytes";
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
outputLinesArr.push(line);
|
|
142
|
+
outputBytesCount += lineBytes;
|
|
143
|
+
}
|
|
144
|
+
// If we exited due to line limit
|
|
145
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
146
|
+
truncatedBy = "lines";
|
|
147
|
+
}
|
|
148
|
+
const outputContent = outputLinesArr.join("\n");
|
|
149
|
+
const finalOutputBytes = utf8ByteLength(outputContent);
|
|
150
|
+
return {
|
|
151
|
+
content: outputContent,
|
|
152
|
+
truncated: true,
|
|
153
|
+
truncatedBy,
|
|
154
|
+
totalLines,
|
|
155
|
+
totalBytes,
|
|
156
|
+
outputLines: outputLinesArr.length,
|
|
157
|
+
outputBytes: finalOutputBytes,
|
|
158
|
+
lastLinePartial: false,
|
|
159
|
+
firstLineExceedsLimit: false,
|
|
160
|
+
maxLines,
|
|
161
|
+
maxBytes,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Truncate content from the tail (keep last N lines/bytes).
|
|
166
|
+
* Suitable for bash output where you want to see the end (errors, final results).
|
|
167
|
+
*
|
|
168
|
+
* May return partial first line if the last line of original content exceeds byte limit.
|
|
169
|
+
*/
|
|
170
|
+
export function truncateTail(content, options = {}) {
|
|
171
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
172
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
173
|
+
const totalBytes = utf8ByteLength(content);
|
|
174
|
+
const lines = content.split("\n");
|
|
175
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
176
|
+
lines.pop();
|
|
177
|
+
const totalLines = lines.length;
|
|
178
|
+
// Check if no truncation needed
|
|
179
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
180
|
+
return {
|
|
181
|
+
content,
|
|
182
|
+
truncated: false,
|
|
183
|
+
truncatedBy: null,
|
|
184
|
+
totalLines,
|
|
185
|
+
totalBytes,
|
|
186
|
+
outputLines: totalLines,
|
|
187
|
+
outputBytes: totalBytes,
|
|
188
|
+
lastLinePartial: false,
|
|
189
|
+
firstLineExceedsLimit: false,
|
|
190
|
+
maxLines,
|
|
191
|
+
maxBytes,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Work backwards from the end
|
|
195
|
+
const outputLinesArr = [];
|
|
196
|
+
let outputBytesCount = 0;
|
|
197
|
+
let truncatedBy = "lines";
|
|
198
|
+
let lastLinePartial = false;
|
|
199
|
+
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
|
|
200
|
+
const line = lines[i];
|
|
201
|
+
const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
|
|
202
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
203
|
+
truncatedBy = "bytes";
|
|
204
|
+
// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
|
|
205
|
+
// take the end of the line (partial)
|
|
206
|
+
if (outputLinesArr.length === 0) {
|
|
207
|
+
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
|
208
|
+
outputLinesArr.unshift(truncatedLine);
|
|
209
|
+
outputBytesCount = utf8ByteLength(truncatedLine);
|
|
210
|
+
lastLinePartial = true;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
outputLinesArr.unshift(line);
|
|
215
|
+
outputBytesCount += lineBytes;
|
|
216
|
+
}
|
|
217
|
+
// If we exited due to line limit
|
|
218
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
219
|
+
truncatedBy = "lines";
|
|
220
|
+
}
|
|
221
|
+
const outputContent = outputLinesArr.join("\n");
|
|
222
|
+
const finalOutputBytes = utf8ByteLength(outputContent);
|
|
223
|
+
return {
|
|
224
|
+
content: outputContent,
|
|
225
|
+
truncated: true,
|
|
226
|
+
truncatedBy,
|
|
227
|
+
totalLines,
|
|
228
|
+
totalBytes,
|
|
229
|
+
outputLines: outputLinesArr.length,
|
|
230
|
+
outputBytes: finalOutputBytes,
|
|
231
|
+
lastLinePartial,
|
|
232
|
+
firstLineExceedsLimit: false,
|
|
233
|
+
maxLines,
|
|
234
|
+
maxBytes,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Truncate a string to fit within a byte limit (from the end).
|
|
239
|
+
* Handles multi-byte UTF-8 characters correctly.
|
|
240
|
+
*/
|
|
241
|
+
function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
242
|
+
if (maxBytes <= 0)
|
|
243
|
+
return "";
|
|
244
|
+
let outputBytes = 0;
|
|
245
|
+
let start = str.length;
|
|
246
|
+
let needsReplacement = false;
|
|
247
|
+
for (let i = str.length; i > 0;) {
|
|
248
|
+
let characterStart = i - 1;
|
|
249
|
+
const code = str.charCodeAt(characterStart);
|
|
250
|
+
let characterBytes;
|
|
251
|
+
let unpairedSurrogate = false;
|
|
252
|
+
if (code >= 0xdc00 && code <= 0xdfff && characterStart > 0) {
|
|
253
|
+
const previous = str.charCodeAt(characterStart - 1);
|
|
254
|
+
if (previous >= 0xd800 && previous <= 0xdbff) {
|
|
255
|
+
characterStart--;
|
|
256
|
+
characterBytes = 4;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
characterBytes = 3;
|
|
260
|
+
unpairedSurrogate = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (code >= 0xd800 && code <= 0xdfff) {
|
|
264
|
+
characterBytes = 3;
|
|
265
|
+
unpairedSurrogate = true;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
characterBytes = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
|
|
269
|
+
}
|
|
270
|
+
if (outputBytes + characterBytes > maxBytes)
|
|
271
|
+
break;
|
|
272
|
+
outputBytes += characterBytes;
|
|
273
|
+
start = characterStart;
|
|
274
|
+
needsReplacement ||= unpairedSurrogate;
|
|
275
|
+
i = characterStart;
|
|
276
|
+
}
|
|
277
|
+
const output = str.slice(start);
|
|
278
|
+
return needsReplacement ? replaceUnpairedSurrogates(output) : output;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Truncate a single line to max characters, adding [truncated] suffix.
|
|
282
|
+
* Used for grep match lines.
|
|
283
|
+
*/
|
|
284
|
+
export function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
|
|
285
|
+
if (line.length <= maxChars) {
|
|
286
|
+
return { text: line, wasTruncated: false };
|
|
287
|
+
}
|
|
288
|
+
return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
|
|
289
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Core Agent
|
|
2
|
+
export * from "./agent.js";
|
|
3
|
+
// Loop functions
|
|
4
|
+
export * from "./agent-loop.js";
|
|
5
|
+
export * from "./harness/agent-harness.js";
|
|
6
|
+
export { collectEntriesForBranchSummary, generateBranchSummary, prepareBranchEntries, } from "./harness/compaction/branch-summarization.js";
|
|
7
|
+
export { calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, estimateTokens, findCutPoint, findTurnStartIndex, generateSummary, getLastAssistantUsage, prepareCompaction, serializeConversation, shouldCompact, } from "./harness/compaction/compaction.js";
|
|
8
|
+
export * from "./harness/messages.js";
|
|
9
|
+
export * from "./harness/prompt-templates.js";
|
|
10
|
+
export * from "./harness/session/jsonl-repo.js";
|
|
11
|
+
export * from "./harness/session/memory-repo.js";
|
|
12
|
+
export * from "./harness/session/repo-utils.js";
|
|
13
|
+
export * from "./harness/session/session.js";
|
|
14
|
+
export { uuidv7 } from "./harness/session/uuid.js";
|
|
15
|
+
export * from "./harness/skills.js";
|
|
16
|
+
export * from "./harness/system-prompt.js";
|
|
17
|
+
// Harness
|
|
18
|
+
export * from "./harness/types.js";
|
|
19
|
+
export * from "./harness/utils/shell-output.js";
|
|
20
|
+
export * from "./harness/utils/truncate.js";
|
|
21
|
+
// Proxy utilities
|
|
22
|
+
export * from "./proxy.js";
|
|
23
|
+
// Types
|
|
24
|
+
export * from "./types.js";
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy stream function for apps that route LLM calls through a server.
|
|
3
|
+
* The server manages auth and proxies requests to LLM providers.
|
|
4
|
+
*/
|
|
5
|
+
// Internal import for JSON parsing utility
|
|
6
|
+
import { EventStream, parseStreamingJson, } from "../ai/index.js";
|
|
7
|
+
// Create stream class matching ProxyMessageEventStream
|
|
8
|
+
class ProxyMessageEventStream extends EventStream {
|
|
9
|
+
constructor() {
|
|
10
|
+
super((event) => event.type === "done" || event.type === "error", (event) => {
|
|
11
|
+
if (event.type === "done")
|
|
12
|
+
return event.message;
|
|
13
|
+
if (event.type === "error")
|
|
14
|
+
return event.error;
|
|
15
|
+
throw new Error("Unexpected event type");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Stream function that proxies through a server instead of calling LLM providers directly.
|
|
21
|
+
* The server strips the partial field from delta events to reduce bandwidth.
|
|
22
|
+
* We reconstruct the partial message client-side.
|
|
23
|
+
*
|
|
24
|
+
* Use this as the `streamFn` option when creating an Agent that needs to go through a proxy.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const agent = new Agent({
|
|
29
|
+
* streamFn: (model, context, options) =>
|
|
30
|
+
* streamProxy(model, context, {
|
|
31
|
+
* ...options,
|
|
32
|
+
* authToken: await getAuthToken(),
|
|
33
|
+
* proxyUrl: "https://genai.example.com",
|
|
34
|
+
* }),
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function buildProxyRequestOptions(options) {
|
|
39
|
+
return {
|
|
40
|
+
temperature: options.temperature,
|
|
41
|
+
maxTokens: options.maxTokens,
|
|
42
|
+
reasoning: options.reasoning,
|
|
43
|
+
cacheRetention: options.cacheRetention,
|
|
44
|
+
sessionId: options.sessionId,
|
|
45
|
+
headers: options.headers,
|
|
46
|
+
metadata: options.metadata,
|
|
47
|
+
transport: options.transport,
|
|
48
|
+
thinkingBudgets: options.thinkingBudgets,
|
|
49
|
+
maxRetryDelayMs: options.maxRetryDelayMs,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function streamProxy(model, context, options) {
|
|
53
|
+
const stream = new ProxyMessageEventStream();
|
|
54
|
+
(async () => {
|
|
55
|
+
// Initialize the partial message that we'll build up from events
|
|
56
|
+
const partial = {
|
|
57
|
+
role: "assistant",
|
|
58
|
+
stopReason: "stop",
|
|
59
|
+
content: [],
|
|
60
|
+
api: model.api,
|
|
61
|
+
provider: model.provider,
|
|
62
|
+
model: model.id,
|
|
63
|
+
usage: {
|
|
64
|
+
input: 0,
|
|
65
|
+
output: 0,
|
|
66
|
+
cacheRead: 0,
|
|
67
|
+
cacheWrite: 0,
|
|
68
|
+
totalTokens: 0,
|
|
69
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
70
|
+
},
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
let reader;
|
|
74
|
+
const abortHandler = () => {
|
|
75
|
+
if (reader) {
|
|
76
|
+
reader.cancel("Request aborted by user").catch(() => { });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (options.signal) {
|
|
80
|
+
options.signal.addEventListener("abort", abortHandler);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${options.proxyUrl}/api/stream`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${options.authToken}`,
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model,
|
|
91
|
+
context,
|
|
92
|
+
options: buildProxyRequestOptions(options),
|
|
93
|
+
}),
|
|
94
|
+
signal: options.signal,
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
let errorMessage = `Proxy error: ${response.status} ${response.statusText}`;
|
|
98
|
+
try {
|
|
99
|
+
const errorData = (await response.json());
|
|
100
|
+
if (errorData.error) {
|
|
101
|
+
errorMessage = `Proxy error: ${errorData.error}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Couldn't parse error response
|
|
106
|
+
}
|
|
107
|
+
throw new Error(errorMessage);
|
|
108
|
+
}
|
|
109
|
+
reader = response.body.getReader();
|
|
110
|
+
const decoder = new TextDecoder();
|
|
111
|
+
let buffer = "";
|
|
112
|
+
while (true) {
|
|
113
|
+
const { done, value } = await reader.read();
|
|
114
|
+
if (done)
|
|
115
|
+
break;
|
|
116
|
+
if (options.signal?.aborted) {
|
|
117
|
+
throw new Error("Request aborted by user");
|
|
118
|
+
}
|
|
119
|
+
buffer += decoder.decode(value, { stream: true });
|
|
120
|
+
const lines = buffer.split("\n");
|
|
121
|
+
buffer = lines.pop() || "";
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (line.startsWith("data: ")) {
|
|
124
|
+
const data = line.slice(6).trim();
|
|
125
|
+
if (data) {
|
|
126
|
+
const proxyEvent = JSON.parse(data);
|
|
127
|
+
const event = processProxyEvent(proxyEvent, partial);
|
|
128
|
+
if (event) {
|
|
129
|
+
stream.push(event);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (options.signal?.aborted) {
|
|
136
|
+
throw new Error("Request aborted by user");
|
|
137
|
+
}
|
|
138
|
+
stream.end();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
142
|
+
const reason = options.signal?.aborted ? "aborted" : "error";
|
|
143
|
+
partial.stopReason = reason;
|
|
144
|
+
partial.errorMessage = errorMessage;
|
|
145
|
+
stream.push({
|
|
146
|
+
type: "error",
|
|
147
|
+
reason,
|
|
148
|
+
error: partial,
|
|
149
|
+
});
|
|
150
|
+
stream.end();
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
if (options.signal) {
|
|
154
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
return stream;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Process a proxy event and update the partial message.
|
|
162
|
+
*/
|
|
163
|
+
function processProxyEvent(proxyEvent, partial) {
|
|
164
|
+
switch (proxyEvent.type) {
|
|
165
|
+
case "start":
|
|
166
|
+
return { type: "start", partial };
|
|
167
|
+
case "text_start":
|
|
168
|
+
partial.content[proxyEvent.contentIndex] = { type: "text", text: "" };
|
|
169
|
+
return { type: "text_start", contentIndex: proxyEvent.contentIndex, partial };
|
|
170
|
+
case "text_delta": {
|
|
171
|
+
const content = partial.content[proxyEvent.contentIndex];
|
|
172
|
+
if (content?.type === "text") {
|
|
173
|
+
content.text += proxyEvent.delta;
|
|
174
|
+
return {
|
|
175
|
+
type: "text_delta",
|
|
176
|
+
contentIndex: proxyEvent.contentIndex,
|
|
177
|
+
delta: proxyEvent.delta,
|
|
178
|
+
partial,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
throw new Error("Received text_delta for non-text content");
|
|
182
|
+
}
|
|
183
|
+
case "text_end": {
|
|
184
|
+
const content = partial.content[proxyEvent.contentIndex];
|
|
185
|
+
if (content?.type === "text") {
|
|
186
|
+
content.textSignature = proxyEvent.contentSignature;
|
|
187
|
+
return {
|
|
188
|
+
type: "text_end",
|
|
189
|
+
contentIndex: proxyEvent.contentIndex,
|
|
190
|
+
content: content.text,
|
|
191
|
+
partial,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
throw new Error("Received text_end for non-text content");
|
|
195
|
+
}
|
|
196
|
+
case "thinking_start":
|
|
197
|
+
partial.content[proxyEvent.contentIndex] = { type: "thinking", thinking: "" };
|
|
198
|
+
return { type: "thinking_start", contentIndex: proxyEvent.contentIndex, partial };
|
|
199
|
+
case "thinking_delta": {
|
|
200
|
+
const content = partial.content[proxyEvent.contentIndex];
|
|
201
|
+
if (content?.type === "thinking") {
|
|
202
|
+
content.thinking += proxyEvent.delta;
|
|
203
|
+
return {
|
|
204
|
+
type: "thinking_delta",
|
|
205
|
+
contentIndex: proxyEvent.contentIndex,
|
|
206
|
+
delta: proxyEvent.delta,
|
|
207
|
+
partial,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
throw new Error("Received thinking_delta for non-thinking content");
|
|
211
|
+
}
|
|
212
|
+
case "thinking_end": {
|
|
213
|
+
const content = partial.content[proxyEvent.contentIndex];
|
|
214
|
+
if (content?.type === "thinking") {
|
|
215
|
+
content.thinkingSignature = proxyEvent.contentSignature;
|
|
216
|
+
return {
|
|
217
|
+
type: "thinking_end",
|
|
218
|
+
contentIndex: proxyEvent.contentIndex,
|
|
219
|
+
content: content.thinking,
|
|
220
|
+
partial,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Received thinking_end for non-thinking content");
|
|
224
|
+
}
|
|
225
|
+
case "toolcall_start":
|
|
226
|
+
partial.content[proxyEvent.contentIndex] = {
|
|
227
|
+
type: "toolCall",
|
|
228
|
+
id: proxyEvent.id,
|
|
229
|
+
name: proxyEvent.toolName,
|
|
230
|
+
arguments: {},
|
|
231
|
+
partialJson: "",
|
|
232
|
+
};
|
|
233
|
+
return { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
|
|
234
|
+
case "toolcall_delta": {
|
|
235
|
+
const content = partial.content[proxyEvent.contentIndex];
|
|
236
|
+
if (content?.type === "toolCall") {
|
|
237
|
+
content.partialJson += proxyEvent.delta;
|
|
238
|
+
content.arguments = parseStreamingJson(content.partialJson) || {};
|
|
239
|
+
partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
|
|
240
|
+
return {
|
|
241
|
+
type: "toolcall_delta",
|
|
242
|
+
contentIndex: proxyEvent.contentIndex,
|
|
243
|
+
delta: proxyEvent.delta,
|
|
244
|
+
partial,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
throw new Error("Received toolcall_delta for non-toolCall content");
|
|
248
|
+
}
|
|
249
|
+
case "toolcall_end": {
|
|
250
|
+
const content = partial.content[proxyEvent.contentIndex];
|
|
251
|
+
if (content?.type === "toolCall") {
|
|
252
|
+
delete content.partialJson;
|
|
253
|
+
return {
|
|
254
|
+
type: "toolcall_end",
|
|
255
|
+
contentIndex: proxyEvent.contentIndex,
|
|
256
|
+
toolCall: content,
|
|
257
|
+
partial,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
case "done":
|
|
263
|
+
partial.stopReason = proxyEvent.reason;
|
|
264
|
+
partial.usage = proxyEvent.usage;
|
|
265
|
+
return { type: "done", reason: proxyEvent.reason, message: partial };
|
|
266
|
+
case "error":
|
|
267
|
+
partial.stopReason = proxyEvent.reason;
|
|
268
|
+
partial.errorMessage = proxyEvent.errorMessage;
|
|
269
|
+
partial.usage = proxyEvent.usage;
|
|
270
|
+
return { type: "error", reason: proxyEvent.reason, error: partial };
|
|
271
|
+
default: {
|
|
272
|
+
const _exhaustiveCheck = proxyEvent;
|
|
273
|
+
console.warn(`Unhandled proxy event type: ${proxyEvent.type}`);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const apiProviderRegistry = new Map();
|
|
2
|
+
function wrapStream(api, stream) {
|
|
3
|
+
return (model, context, options) => {
|
|
4
|
+
if (model.api !== api) {
|
|
5
|
+
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
|
6
|
+
}
|
|
7
|
+
return stream(model, context, options);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function wrapStreamSimple(api, streamSimple) {
|
|
11
|
+
return (model, context, options) => {
|
|
12
|
+
if (model.api !== api) {
|
|
13
|
+
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
|
14
|
+
}
|
|
15
|
+
return streamSimple(model, context, options);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function registerApiProvider(provider, sourceId) {
|
|
19
|
+
apiProviderRegistry.set(provider.api, {
|
|
20
|
+
provider: {
|
|
21
|
+
api: provider.api,
|
|
22
|
+
stream: wrapStream(provider.api, provider.stream),
|
|
23
|
+
streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
|
|
24
|
+
},
|
|
25
|
+
sourceId,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function getApiProvider(api) {
|
|
29
|
+
return apiProviderRegistry.get(api)?.provider;
|
|
30
|
+
}
|
|
31
|
+
export function getApiProviders() {
|
|
32
|
+
return Array.from(apiProviderRegistry.values(), (entry) => entry.provider);
|
|
33
|
+
}
|
|
34
|
+
export function unregisterApiProviders(sourceId) {
|
|
35
|
+
for (const [api, entry] of apiProviderRegistry.entries()) {
|
|
36
|
+
if (entry.sourceId === sourceId) {
|
|
37
|
+
apiProviderRegistry.delete(api);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function clearApiProviders() {
|
|
42
|
+
apiProviderRegistry.clear();
|
|
43
|
+
}
|