agent-sh 0.14.8 → 0.14.9
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/agent/agent-loop.d.ts +0 -4
- package/dist/agent/agent-loop.js +8 -166
- package/dist/agent/entry-format.d.ts +5 -0
- package/dist/agent/entry-format.js +9 -0
- package/dist/agent/extensions/rolling-history/constants.d.ts +1 -0
- package/dist/agent/extensions/rolling-history/constants.js +1 -0
- package/dist/agent/extensions/rolling-history/index.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/index.js +203 -0
- package/dist/agent/extensions/rolling-history/recall.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/recall.js +122 -0
- package/dist/agent/extensions/rolling-history/strategy.d.ts +70 -0
- package/dist/agent/extensions/rolling-history/strategy.js +336 -0
- package/dist/agent/host-types.d.ts +0 -3
- package/dist/agent/index.js +44 -5
- package/dist/agent/live-view.d.ts +57 -0
- package/dist/agent/live-view.js +238 -0
- package/dist/agent/llm-client.d.ts +1 -0
- package/dist/agent/llm-client.js +1 -1
- package/dist/agent/session-store.d.ts +90 -0
- package/dist/agent/session-store.js +288 -0
- package/dist/agent/store.d.ts +74 -0
- package/dist/agent/store.js +284 -0
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/tool-protocol.d.ts +11 -11
- package/dist/cli/index.js +4 -2
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -1
- package/dist/core/settings.d.ts +5 -1
- package/dist/core/settings.js +62 -1
- package/dist/extensions/index.d.ts +1 -0
- package/dist/shell/events.d.ts +1 -0
- package/dist/shell/input-handler.js +4 -0
- package/dist/shell/tui-renderer.js +5 -2
- package/dist/utils/diff-renderer.js +9 -7
- package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/capture.ts +1 -1
- package/examples/extensions/ashi/src/cli.ts +3 -4
- package/examples/extensions/ashi/src/compaction.ts +6 -2
- package/examples/extensions/ashi/src/frontend.ts +13 -13
- package/examples/extensions/ashi/src/multi-session-store.ts +35 -12
- package/examples/extensions/ashi/src/session-commands.ts +1 -1
- package/examples/extensions/ashi/src/user-shell-intents.ts +17 -0
- package/package.json +13 -1
- package/dist/agent/conversation-state.d.ts +0 -142
- package/dist/agent/conversation-state.js +0 -788
- package/dist/agent/history-file.d.ts +0 -81
- package/dist/agent/history-file.js +0 -271
- package/examples/extensions/ashi/src/session-store.ts +0 -363
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { type NuclearEntry } from "./nuclear-form.js";
|
|
2
|
-
export interface HistoryAdapter {
|
|
3
|
-
append(entries: NuclearEntry[]): Promise<void>;
|
|
4
|
-
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
5
|
-
search(query: string): Promise<{
|
|
6
|
-
entry: NuclearEntry;
|
|
7
|
-
line: string;
|
|
8
|
-
}[]>;
|
|
9
|
-
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
10
|
-
/** Walk parent pointers from a leaf back to the root. Tree-aware adapters only. */
|
|
11
|
-
getBranch?(leafSeq: number): Promise<NuclearEntry[]>;
|
|
12
|
-
/** Return every entry, including sibling branches. Tree-aware adapters only. */
|
|
13
|
-
getTree?(): Promise<NuclearEntry[]>;
|
|
14
|
-
/** Move the active leaf for the next append. Tree-aware adapters only. */
|
|
15
|
-
setLeaf?(seq: number): void;
|
|
16
|
-
}
|
|
17
|
-
export declare class InMemoryHistory implements HistoryAdapter {
|
|
18
|
-
private entries;
|
|
19
|
-
constructor(initial?: NuclearEntry[]);
|
|
20
|
-
append(entries: NuclearEntry[]): Promise<void>;
|
|
21
|
-
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
22
|
-
search(query: string): Promise<{
|
|
23
|
-
entry: NuclearEntry;
|
|
24
|
-
line: string;
|
|
25
|
-
}[]>;
|
|
26
|
-
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
27
|
-
}
|
|
28
|
-
export declare class NoopHistory implements HistoryAdapter {
|
|
29
|
-
append(): Promise<void>;
|
|
30
|
-
readRecent(): Promise<NuclearEntry[]>;
|
|
31
|
-
search(): Promise<{
|
|
32
|
-
entry: NuclearEntry;
|
|
33
|
-
line: string;
|
|
34
|
-
}[]>;
|
|
35
|
-
findBySeq(): Promise<NuclearEntry | null>;
|
|
36
|
-
}
|
|
37
|
-
export declare class HistoryFile implements HistoryAdapter {
|
|
38
|
-
readonly instanceId: string;
|
|
39
|
-
private filePath;
|
|
40
|
-
private lockPath;
|
|
41
|
-
constructor(opts?: {
|
|
42
|
-
filePath?: string;
|
|
43
|
-
instanceId?: string;
|
|
44
|
-
});
|
|
45
|
-
/**
|
|
46
|
-
* Append entries atomically. Uses O_APPEND for concurrency safety.
|
|
47
|
-
* Triggers truncation check after writing.
|
|
48
|
-
*/
|
|
49
|
-
append(entries: NuclearEntry[]): Promise<void>;
|
|
50
|
-
/**
|
|
51
|
-
* Read the most recent N entries from the history file, filtered.
|
|
52
|
-
* Read-only tool calls (read_file, grep, glob, ls) are excluded so
|
|
53
|
-
* the returned entries are all meaningful conversation turns.
|
|
54
|
-
*/
|
|
55
|
-
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
56
|
-
/**
|
|
57
|
-
* Search history entries by regex/keyword, scanning the file from the
|
|
58
|
-
* end. Caps at ~20 MB of content to bound cost on 100 MB history files.
|
|
59
|
-
*/
|
|
60
|
-
search(query: string): Promise<{
|
|
61
|
-
entry: NuclearEntry;
|
|
62
|
-
line: string;
|
|
63
|
-
}[]>;
|
|
64
|
-
/** Find a single entry by sequence number, streaming from the file end. */
|
|
65
|
-
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
66
|
-
getSize(): Promise<number>;
|
|
67
|
-
/**
|
|
68
|
-
* Yield lines from the file in reverse order (newest-first). Buffers
|
|
69
|
-
* pre-first-newline bytes across chunks to stitch lines that straddle
|
|
70
|
-
* a boundary; carries raw bytes (not strings) so UTF-8 characters split
|
|
71
|
-
* by a chunk boundary are never decoded mid-codepoint.
|
|
72
|
-
*/
|
|
73
|
-
private streamReverseLines;
|
|
74
|
-
/**
|
|
75
|
-
* Truncate from the front if file exceeds historyMaxBytes.
|
|
76
|
-
* Uses a lock file for the rewrite operation.
|
|
77
|
-
*/
|
|
78
|
-
private maybeTruncate;
|
|
79
|
-
private acquireLock;
|
|
80
|
-
private releaseLock;
|
|
81
|
-
}
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistent history file — append-only JSONL at ~/.agent-sh/history.
|
|
3
|
-
*
|
|
4
|
-
* Multiple agent-sh instances can write concurrently — each line is under
|
|
5
|
-
* PIPE_BUF so O_APPEND writes are atomic. Only truncation (which rewrites
|
|
6
|
-
* the file) uses a lock file for safety.
|
|
7
|
-
*/
|
|
8
|
-
import * as fs from "node:fs/promises";
|
|
9
|
-
import * as fss from "node:fs";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import * as crypto from "node:crypto";
|
|
12
|
-
import { CONFIG_DIR, getSettings } from "../core/settings.js";
|
|
13
|
-
import { serializeEntry, deserializeEntry, isReadOnly, compileSearchRegex, matchEntry, } from "./nuclear-form.js";
|
|
14
|
-
const HISTORY_PATH = path.join(CONFIG_DIR, "history");
|
|
15
|
-
const LOCK_STALE_MS = 10_000; // consider lock stale after 10s
|
|
16
|
-
export class InMemoryHistory {
|
|
17
|
-
entries;
|
|
18
|
-
constructor(initial = []) {
|
|
19
|
-
this.entries = [...initial];
|
|
20
|
-
}
|
|
21
|
-
async append(entries) {
|
|
22
|
-
this.entries.push(...entries);
|
|
23
|
-
}
|
|
24
|
-
async readRecent(maxEntries) {
|
|
25
|
-
const filtered = this.entries.filter((e) => !isReadOnly(e));
|
|
26
|
-
return maxEntries ? filtered.slice(-maxEntries) : filtered;
|
|
27
|
-
}
|
|
28
|
-
async search(query) {
|
|
29
|
-
if (!query.trim())
|
|
30
|
-
return [];
|
|
31
|
-
const re = compileSearchRegex(query);
|
|
32
|
-
const out = [];
|
|
33
|
-
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
34
|
-
const m = matchEntry(this.entries[i], re);
|
|
35
|
-
if (m)
|
|
36
|
-
out.push(m);
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
async findBySeq(seq) {
|
|
41
|
-
return this.entries.find((e) => e.seq === seq) ?? null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export class NoopHistory {
|
|
45
|
-
async append() { }
|
|
46
|
-
async readRecent() { return []; }
|
|
47
|
-
async search() { return []; }
|
|
48
|
-
async findBySeq() { return null; }
|
|
49
|
-
}
|
|
50
|
-
export class HistoryFile {
|
|
51
|
-
instanceId;
|
|
52
|
-
filePath;
|
|
53
|
-
lockPath;
|
|
54
|
-
constructor(opts) {
|
|
55
|
-
this.filePath = opts?.filePath ?? HISTORY_PATH;
|
|
56
|
-
this.lockPath = this.filePath + ".lock";
|
|
57
|
-
this.instanceId = opts?.instanceId ?? crypto.randomBytes(2).toString("hex");
|
|
58
|
-
// Custom paths may target a dir that doesn't exist yet; create sync so
|
|
59
|
-
// the first append() can't race with the mkdir.
|
|
60
|
-
try {
|
|
61
|
-
fss.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
catch { /* ignore */ }
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Append entries atomically. Uses O_APPEND for concurrency safety.
|
|
67
|
-
* Triggers truncation check after writing.
|
|
68
|
-
*/
|
|
69
|
-
async append(entries) {
|
|
70
|
-
if (entries.length === 0)
|
|
71
|
-
return;
|
|
72
|
-
const lines = entries.map((e) => serializeEntry(e) + "\n").join("");
|
|
73
|
-
await fs.appendFile(this.filePath, lines, { flag: "a" });
|
|
74
|
-
await this.maybeTruncate();
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Read the most recent N entries from the history file, filtered.
|
|
78
|
-
* Read-only tool calls (read_file, grep, glob, ls) are excluded so
|
|
79
|
-
* the returned entries are all meaningful conversation turns.
|
|
80
|
-
*/
|
|
81
|
-
async readRecent(maxEntries) {
|
|
82
|
-
maxEntries ??= getSettings().historyStartupEntries;
|
|
83
|
-
const want = maxEntries * 3 + 10;
|
|
84
|
-
const recent = []; // newest-first
|
|
85
|
-
for await (const line of this.streamReverseLines()) {
|
|
86
|
-
const entry = deserializeEntry(line);
|
|
87
|
-
if (entry && !isReadOnly(entry))
|
|
88
|
-
recent.push(entry);
|
|
89
|
-
if (recent.length >= want)
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
// Caller expects oldest-to-newest order.
|
|
93
|
-
return recent.reverse().slice(-maxEntries);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Search history entries by regex/keyword, scanning the file from the
|
|
97
|
-
* end. Caps at ~20 MB of content to bound cost on 100 MB history files.
|
|
98
|
-
*/
|
|
99
|
-
async search(query) {
|
|
100
|
-
if (!query.trim())
|
|
101
|
-
return [];
|
|
102
|
-
const regex = compileSearchRegex(query);
|
|
103
|
-
const budgetBytes = 20 * 1024 * 1024;
|
|
104
|
-
let scanned = 0;
|
|
105
|
-
const results = [];
|
|
106
|
-
for await (const line of this.streamReverseLines()) {
|
|
107
|
-
scanned += line.length + 1;
|
|
108
|
-
if (scanned > budgetBytes)
|
|
109
|
-
break;
|
|
110
|
-
const entry = deserializeEntry(line);
|
|
111
|
-
if (!entry)
|
|
112
|
-
continue;
|
|
113
|
-
const m = matchEntry(entry, regex);
|
|
114
|
-
if (m)
|
|
115
|
-
results.push(m);
|
|
116
|
-
}
|
|
117
|
-
return results;
|
|
118
|
-
}
|
|
119
|
-
/** Find a single entry by sequence number, streaming from the file end. */
|
|
120
|
-
async findBySeq(seq) {
|
|
121
|
-
for await (const line of this.streamReverseLines()) {
|
|
122
|
-
const entry = deserializeEntry(line);
|
|
123
|
-
if (entry && entry.seq === seq)
|
|
124
|
-
return entry;
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
async getSize() {
|
|
129
|
-
try {
|
|
130
|
-
const stat = await fs.stat(this.filePath);
|
|
131
|
-
return stat.size;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Yield lines from the file in reverse order (newest-first). Buffers
|
|
139
|
-
* pre-first-newline bytes across chunks to stitch lines that straddle
|
|
140
|
-
* a boundary; carries raw bytes (not strings) so UTF-8 characters split
|
|
141
|
-
* by a chunk boundary are never decoded mid-codepoint.
|
|
142
|
-
*/
|
|
143
|
-
async *streamReverseLines(chunkBytes = 1 << 20) {
|
|
144
|
-
let handle;
|
|
145
|
-
let fileSize;
|
|
146
|
-
try {
|
|
147
|
-
const stat = await fs.stat(this.filePath);
|
|
148
|
-
fileSize = stat.size;
|
|
149
|
-
if (fileSize === 0)
|
|
150
|
-
return;
|
|
151
|
-
handle = await fs.open(this.filePath, "r");
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
let position = fileSize;
|
|
158
|
-
let pending = Buffer.alloc(0);
|
|
159
|
-
while (position > 0) {
|
|
160
|
-
const readSize = Math.min(chunkBytes, position);
|
|
161
|
-
position -= readSize;
|
|
162
|
-
const buf = Buffer.alloc(readSize);
|
|
163
|
-
await handle.read(buf, 0, readSize, position);
|
|
164
|
-
// pending: start-bytes of a line whose first \n lives in this chunk.
|
|
165
|
-
const combined = Buffer.concat([buf, pending]);
|
|
166
|
-
const newlineIdxs = [];
|
|
167
|
-
for (let i = 0; i < combined.length; i++) {
|
|
168
|
-
if (combined[i] === 0x0A)
|
|
169
|
-
newlineIdxs.push(i);
|
|
170
|
-
}
|
|
171
|
-
if (newlineIdxs.length === 0) {
|
|
172
|
-
pending = combined;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
const firstNl = newlineIdxs[0];
|
|
176
|
-
const lastNl = newlineIdxs[newlineIdxs.length - 1];
|
|
177
|
-
// Post-last-\n: a line straddling into the later chunk (completed
|
|
178
|
-
// here because `pending` was appended at the end of `combined`).
|
|
179
|
-
const trailing = combined.subarray(lastNl + 1);
|
|
180
|
-
if (trailing.length > 0)
|
|
181
|
-
yield trailing.toString("utf-8");
|
|
182
|
-
for (let i = newlineIdxs.length - 1; i >= 1; i--) {
|
|
183
|
-
const seg = combined.subarray(newlineIdxs[i - 1] + 1, newlineIdxs[i]);
|
|
184
|
-
if (seg.length > 0)
|
|
185
|
-
yield seg.toString("utf-8");
|
|
186
|
-
}
|
|
187
|
-
// Pre-first-\n: partial if there's more file to the left, else complete.
|
|
188
|
-
const leading = combined.subarray(0, firstNl);
|
|
189
|
-
if (position === 0) {
|
|
190
|
-
if (leading.length > 0)
|
|
191
|
-
yield leading.toString("utf-8");
|
|
192
|
-
pending = Buffer.alloc(0);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
pending = leading;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (pending.length > 0)
|
|
199
|
-
yield pending.toString("utf-8");
|
|
200
|
-
}
|
|
201
|
-
finally {
|
|
202
|
-
await handle.close();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// ── Truncation ──────────────────────────────────────────────────
|
|
206
|
-
/**
|
|
207
|
-
* Truncate from the front if file exceeds historyMaxBytes.
|
|
208
|
-
* Uses a lock file for the rewrite operation.
|
|
209
|
-
*/
|
|
210
|
-
async maybeTruncate() {
|
|
211
|
-
const maxBytes = getSettings().historyMaxBytes;
|
|
212
|
-
const size = await this.getSize();
|
|
213
|
-
// Only truncate when significantly over (150%) to avoid frequent rewrites
|
|
214
|
-
if (size <= maxBytes * 1.5)
|
|
215
|
-
return;
|
|
216
|
-
const acquired = await this.acquireLock();
|
|
217
|
-
if (!acquired)
|
|
218
|
-
return; // another process is truncating
|
|
219
|
-
try {
|
|
220
|
-
let content;
|
|
221
|
-
try {
|
|
222
|
-
content = await fs.readFile(this.filePath, "utf-8");
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const lines = content.split("\n").filter(Boolean);
|
|
228
|
-
// Drop oldest lines until under maxBytes
|
|
229
|
-
let totalBytes = Buffer.byteLength(content, "utf-8");
|
|
230
|
-
let dropCount = 0;
|
|
231
|
-
while (totalBytes > maxBytes && dropCount < lines.length - 1) {
|
|
232
|
-
totalBytes -= Buffer.byteLength(lines[dropCount] + "\n", "utf-8");
|
|
233
|
-
dropCount++;
|
|
234
|
-
}
|
|
235
|
-
if (dropCount === 0)
|
|
236
|
-
return;
|
|
237
|
-
const remaining = lines.slice(dropCount).join("\n") + "\n";
|
|
238
|
-
// Atomic rewrite: write temp → rename
|
|
239
|
-
const tmpPath = this.filePath + ".tmp." + process.pid;
|
|
240
|
-
await fs.writeFile(tmpPath, remaining);
|
|
241
|
-
await fs.rename(tmpPath, this.filePath);
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
await this.releaseLock();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
async acquireLock() {
|
|
248
|
-
try {
|
|
249
|
-
// Check for stale lock
|
|
250
|
-
try {
|
|
251
|
-
const stat = await fs.stat(this.lockPath);
|
|
252
|
-
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
253
|
-
await fs.unlink(this.lockPath).catch(() => { });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
// Lock doesn't exist — good
|
|
258
|
-
}
|
|
259
|
-
// O_EXCL ensures atomicity
|
|
260
|
-
const fd = await fs.open(this.lockPath, fss.constants.O_CREAT | fss.constants.O_EXCL | fss.constants.O_WRONLY);
|
|
261
|
-
await fd.close();
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
return false; // lock held by another process
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
async releaseLock() {
|
|
269
|
-
await fs.unlink(this.lockPath).catch(() => { });
|
|
270
|
-
}
|
|
271
|
-
}
|