agent-sh 0.14.7 → 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 +46 -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/auth/discover.js +18 -1
- 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/strategies/bash.js +6 -2
- 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 -10
- 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/examples/extensions/ollama.ts +3 -2
- package/examples/extensions/opencode-provider.ts +1 -2
- package/examples/extensions/zai-coding-plan.ts +1 -2
- 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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface Entry {
|
|
2
|
+
id: string;
|
|
3
|
+
parentId?: string;
|
|
4
|
+
ts: number;
|
|
5
|
+
kind: string;
|
|
6
|
+
payload: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface AppendOpts {
|
|
9
|
+
/** Memory-only; never persisted. */
|
|
10
|
+
ephemeral?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface SearchHit {
|
|
13
|
+
entry: Entry;
|
|
14
|
+
line: string;
|
|
15
|
+
}
|
|
16
|
+
/** Append-only — no edit or delete. Implementations may apply bulk
|
|
17
|
+
* retention (front-truncation, GC), but strategies cannot remove a
|
|
18
|
+
* specific entry. */
|
|
19
|
+
export interface Store {
|
|
20
|
+
append(entries: Entry[], opts?: AppendOpts): Promise<void>;
|
|
21
|
+
findById(id: string): Promise<Entry | null>;
|
|
22
|
+
readRecent(n?: number): Promise<Entry[]>;
|
|
23
|
+
search(query: string): Promise<SearchHit[]>;
|
|
24
|
+
}
|
|
25
|
+
export interface TreeStore extends Store {
|
|
26
|
+
getBranch(leafId?: string): Promise<Entry[]>;
|
|
27
|
+
setLeaf(id: string): void;
|
|
28
|
+
getLeaf(): string;
|
|
29
|
+
}
|
|
30
|
+
export declare function newEntryId(): string;
|
|
31
|
+
export declare function isTreeStore(s: Store): s is TreeStore;
|
|
32
|
+
export declare class NoopStore implements Store {
|
|
33
|
+
append(): Promise<void>;
|
|
34
|
+
findById(): Promise<Entry | null>;
|
|
35
|
+
readRecent(): Promise<Entry[]>;
|
|
36
|
+
search(): Promise<SearchHit[]>;
|
|
37
|
+
}
|
|
38
|
+
export declare class InMemoryStore implements TreeStore {
|
|
39
|
+
private entries;
|
|
40
|
+
private order;
|
|
41
|
+
private leaf;
|
|
42
|
+
constructor(opts?: {
|
|
43
|
+
root?: Entry;
|
|
44
|
+
});
|
|
45
|
+
append(entries: Entry[]): Promise<void>;
|
|
46
|
+
findById(id: string): Promise<Entry | null>;
|
|
47
|
+
readRecent(n?: number): Promise<Entry[]>;
|
|
48
|
+
search(query: string): Promise<SearchHit[]>;
|
|
49
|
+
getBranch(leafId?: string): Promise<Entry[]>;
|
|
50
|
+
setLeaf(id: string): void;
|
|
51
|
+
getLeaf(): string;
|
|
52
|
+
}
|
|
53
|
+
export interface SharedFileStoreOpts {
|
|
54
|
+
filePath: string;
|
|
55
|
+
/** Front-truncate above this size; truncation fires at 150% of the
|
|
56
|
+
* cap to avoid frequent rewrites. */
|
|
57
|
+
maxBytes?: number;
|
|
58
|
+
}
|
|
59
|
+
export declare class SharedFileStore implements Store {
|
|
60
|
+
private filePath;
|
|
61
|
+
private lockPath;
|
|
62
|
+
private maxBytes;
|
|
63
|
+
constructor(opts: SharedFileStoreOpts);
|
|
64
|
+
append(entries: Entry[], opts?: AppendOpts): Promise<void>;
|
|
65
|
+
findById(id: string): Promise<Entry | null>;
|
|
66
|
+
readRecent(n?: number): Promise<Entry[]>;
|
|
67
|
+
search(query: string): Promise<SearchHit[]>;
|
|
68
|
+
/** Yield lines newest-first by reading reverse-chunked blocks,
|
|
69
|
+
* stitching across boundaries. */
|
|
70
|
+
private streamReverseLines;
|
|
71
|
+
private maybeTruncate;
|
|
72
|
+
private acquireLock;
|
|
73
|
+
private releaseLock;
|
|
74
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as crypto from "node:crypto";
|
|
5
|
+
export function newEntryId() {
|
|
6
|
+
return crypto.randomBytes(4).toString("hex");
|
|
7
|
+
}
|
|
8
|
+
export function isTreeStore(s) {
|
|
9
|
+
return (typeof s.setLeaf === "function" &&
|
|
10
|
+
typeof s.getLeaf === "function" &&
|
|
11
|
+
typeof s.getBranch === "function");
|
|
12
|
+
}
|
|
13
|
+
function escapeRegex(s) {
|
|
14
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
|
+
}
|
|
16
|
+
function compileSearchRegex(query) {
|
|
17
|
+
return new RegExp(escapeRegex(query), "i");
|
|
18
|
+
}
|
|
19
|
+
function matchEntry(entry, re) {
|
|
20
|
+
const line = JSON.stringify(entry);
|
|
21
|
+
return re.test(line) ? { entry, line } : null;
|
|
22
|
+
}
|
|
23
|
+
export class NoopStore {
|
|
24
|
+
async append() { }
|
|
25
|
+
async findById() { return null; }
|
|
26
|
+
async readRecent() { return []; }
|
|
27
|
+
async search() { return []; }
|
|
28
|
+
}
|
|
29
|
+
export class InMemoryStore {
|
|
30
|
+
entries = new Map();
|
|
31
|
+
order = [];
|
|
32
|
+
leaf;
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
if (opts?.root) {
|
|
35
|
+
this.entries.set(opts.root.id, opts.root);
|
|
36
|
+
this.order.push(opts.root.id);
|
|
37
|
+
this.leaf = opts.root.id;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.leaf = "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async append(entries) {
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
this.entries.set(e.id, e);
|
|
46
|
+
this.order.push(e.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async findById(id) {
|
|
50
|
+
return this.entries.get(id) ?? null;
|
|
51
|
+
}
|
|
52
|
+
async readRecent(n) {
|
|
53
|
+
const slice = n == null ? this.order : this.order.slice(-n);
|
|
54
|
+
return slice.map((id) => this.entries.get(id));
|
|
55
|
+
}
|
|
56
|
+
async search(query) {
|
|
57
|
+
if (!query.trim())
|
|
58
|
+
return [];
|
|
59
|
+
const re = compileSearchRegex(query);
|
|
60
|
+
const out = [];
|
|
61
|
+
for (let i = this.order.length - 1; i >= 0; i--) {
|
|
62
|
+
const m = matchEntry(this.entries.get(this.order[i]), re);
|
|
63
|
+
if (m)
|
|
64
|
+
out.push(m);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
async getBranch(leafId = this.leaf) {
|
|
69
|
+
const out = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
let cur = leafId;
|
|
72
|
+
while (cur && !seen.has(cur)) {
|
|
73
|
+
seen.add(cur);
|
|
74
|
+
const e = this.entries.get(cur);
|
|
75
|
+
if (!e)
|
|
76
|
+
break;
|
|
77
|
+
out.push(e);
|
|
78
|
+
cur = e.parentId;
|
|
79
|
+
}
|
|
80
|
+
return out.reverse();
|
|
81
|
+
}
|
|
82
|
+
setLeaf(id) {
|
|
83
|
+
if (!this.entries.has(id))
|
|
84
|
+
throw new Error(`unknown entry: ${id}`);
|
|
85
|
+
this.leaf = id;
|
|
86
|
+
}
|
|
87
|
+
getLeaf() {
|
|
88
|
+
return this.leaf;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Multi-writer JSONL Store. O_APPEND with PIPE_BUF-bounded line
|
|
92
|
+
* writes for atomic concurrent appends; lock-based front-truncation
|
|
93
|
+
* for retention; reads stream the tail for cheap recent slices. */
|
|
94
|
+
const LOCK_STALE_MS = 10_000;
|
|
95
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
96
|
+
export class SharedFileStore {
|
|
97
|
+
filePath;
|
|
98
|
+
lockPath;
|
|
99
|
+
maxBytes;
|
|
100
|
+
constructor(opts) {
|
|
101
|
+
this.filePath = opts.filePath;
|
|
102
|
+
this.lockPath = opts.filePath + ".lock";
|
|
103
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
104
|
+
try {
|
|
105
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
catch { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
async append(entries, opts) {
|
|
110
|
+
if (entries.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
if (opts?.ephemeral)
|
|
113
|
+
return; // memory-only writes are a no-op on a file-only store
|
|
114
|
+
const lines = entries.map((e) => JSON.stringify(e) + "\n").join("");
|
|
115
|
+
await fsp.appendFile(this.filePath, lines, { flag: "a" });
|
|
116
|
+
await this.maybeTruncate();
|
|
117
|
+
}
|
|
118
|
+
async findById(id) {
|
|
119
|
+
for await (const line of this.streamReverseLines()) {
|
|
120
|
+
try {
|
|
121
|
+
const e = JSON.parse(line);
|
|
122
|
+
if (e.id === id)
|
|
123
|
+
return e;
|
|
124
|
+
}
|
|
125
|
+
catch { /* skip malformed */ }
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
async readRecent(n) {
|
|
130
|
+
const want = n ?? Infinity;
|
|
131
|
+
const recent = []; // newest-first
|
|
132
|
+
for await (const line of this.streamReverseLines()) {
|
|
133
|
+
try {
|
|
134
|
+
const e = JSON.parse(line);
|
|
135
|
+
if (!e.id)
|
|
136
|
+
continue;
|
|
137
|
+
recent.push(e);
|
|
138
|
+
if (recent.length >= want)
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
catch { /* skip malformed */ }
|
|
142
|
+
}
|
|
143
|
+
return recent.reverse();
|
|
144
|
+
}
|
|
145
|
+
async search(query) {
|
|
146
|
+
if (!query.trim())
|
|
147
|
+
return [];
|
|
148
|
+
const re = compileSearchRegex(query);
|
|
149
|
+
const budgetBytes = 20 * 1024 * 1024;
|
|
150
|
+
let scanned = 0;
|
|
151
|
+
const out = [];
|
|
152
|
+
for await (const line of this.streamReverseLines()) {
|
|
153
|
+
scanned += line.length + 1;
|
|
154
|
+
if (scanned > budgetBytes)
|
|
155
|
+
break;
|
|
156
|
+
try {
|
|
157
|
+
const e = JSON.parse(line);
|
|
158
|
+
const m = matchEntry(e, re);
|
|
159
|
+
if (m)
|
|
160
|
+
out.push(m);
|
|
161
|
+
}
|
|
162
|
+
catch { /* skip malformed */ }
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
/** Yield lines newest-first by reading reverse-chunked blocks,
|
|
167
|
+
* stitching across boundaries. */
|
|
168
|
+
async *streamReverseLines(chunkBytes = 1 << 20) {
|
|
169
|
+
let handle;
|
|
170
|
+
let fileSize;
|
|
171
|
+
try {
|
|
172
|
+
const stat = await fsp.stat(this.filePath);
|
|
173
|
+
fileSize = stat.size;
|
|
174
|
+
if (fileSize === 0)
|
|
175
|
+
return;
|
|
176
|
+
handle = await fsp.open(this.filePath, "r");
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
let position = fileSize;
|
|
183
|
+
let pending = Buffer.alloc(0);
|
|
184
|
+
while (position > 0) {
|
|
185
|
+
const readSize = Math.min(chunkBytes, position);
|
|
186
|
+
position -= readSize;
|
|
187
|
+
const buf = Buffer.alloc(readSize);
|
|
188
|
+
await handle.read(buf, 0, readSize, position);
|
|
189
|
+
const combined = Buffer.concat([buf, pending]);
|
|
190
|
+
const newlineIdxs = [];
|
|
191
|
+
for (let i = 0; i < combined.length; i++) {
|
|
192
|
+
if (combined[i] === 0x0A)
|
|
193
|
+
newlineIdxs.push(i);
|
|
194
|
+
}
|
|
195
|
+
if (newlineIdxs.length === 0) {
|
|
196
|
+
pending = combined;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const firstNl = newlineIdxs[0];
|
|
200
|
+
const lastNl = newlineIdxs[newlineIdxs.length - 1];
|
|
201
|
+
const trailing = combined.subarray(lastNl + 1);
|
|
202
|
+
if (trailing.length > 0)
|
|
203
|
+
yield trailing.toString("utf-8");
|
|
204
|
+
for (let i = newlineIdxs.length - 1; i >= 1; i--) {
|
|
205
|
+
const seg = combined.subarray(newlineIdxs[i - 1] + 1, newlineIdxs[i]);
|
|
206
|
+
if (seg.length > 0)
|
|
207
|
+
yield seg.toString("utf-8");
|
|
208
|
+
}
|
|
209
|
+
const leading = combined.subarray(0, firstNl);
|
|
210
|
+
if (position === 0) {
|
|
211
|
+
if (leading.length > 0)
|
|
212
|
+
yield leading.toString("utf-8");
|
|
213
|
+
pending = Buffer.alloc(0);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
pending = leading;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (pending.length > 0)
|
|
220
|
+
yield pending.toString("utf-8");
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await handle.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async maybeTruncate() {
|
|
227
|
+
let size = 0;
|
|
228
|
+
try {
|
|
229
|
+
size = (await fsp.stat(this.filePath)).size;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (size <= this.maxBytes * 1.5)
|
|
235
|
+
return;
|
|
236
|
+
if (!(await this.acquireLock()))
|
|
237
|
+
return;
|
|
238
|
+
try {
|
|
239
|
+
let content;
|
|
240
|
+
try {
|
|
241
|
+
content = await fsp.readFile(this.filePath, "utf-8");
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const lines = content.split("\n").filter(Boolean);
|
|
247
|
+
let totalBytes = Buffer.byteLength(content, "utf-8");
|
|
248
|
+
let dropCount = 0;
|
|
249
|
+
while (totalBytes > this.maxBytes && dropCount < lines.length - 1) {
|
|
250
|
+
totalBytes -= Buffer.byteLength(lines[dropCount] + "\n", "utf-8");
|
|
251
|
+
dropCount++;
|
|
252
|
+
}
|
|
253
|
+
if (dropCount === 0)
|
|
254
|
+
return;
|
|
255
|
+
const remaining = lines.slice(dropCount).join("\n") + "\n";
|
|
256
|
+
const tmpPath = this.filePath + ".tmp." + process.pid;
|
|
257
|
+
await fsp.writeFile(tmpPath, remaining);
|
|
258
|
+
await fsp.rename(tmpPath, this.filePath);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await this.releaseLock();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async acquireLock() {
|
|
265
|
+
try {
|
|
266
|
+
try {
|
|
267
|
+
const stat = await fsp.stat(this.lockPath);
|
|
268
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
269
|
+
await fsp.unlink(this.lockPath).catch(() => { });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch { /* lock absent — good */ }
|
|
273
|
+
const fd = await fsp.open(this.lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
274
|
+
await fd.close();
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async releaseLock() {
|
|
282
|
+
await fsp.unlink(this.lockPath).catch(() => { });
|
|
283
|
+
}
|
|
284
|
+
}
|
package/dist/agent/subagent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { contentText } from "./types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { LiveView } from "./live-view.js";
|
|
3
3
|
import { normalizeToolArgs } from "./normalize-args.js";
|
|
4
4
|
import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
|
|
5
5
|
/**
|
|
@@ -17,7 +17,7 @@ export async function runSubagent(opts) {
|
|
|
17
17
|
parameters: t.input_schema,
|
|
18
18
|
},
|
|
19
19
|
}));
|
|
20
|
-
const conversation = new
|
|
20
|
+
const conversation = new LiveView();
|
|
21
21
|
conversation.addUserMessage(task);
|
|
22
22
|
let fullResponseText = "";
|
|
23
23
|
let iterations = 0;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { ChatCompletionTool } from "./llm-client.js";
|
|
13
13
|
import { type ToolDefinition } from "./types.js";
|
|
14
|
-
import type {
|
|
14
|
+
import type { LiveView } from "./live-view.js";
|
|
15
15
|
export interface PendingToolCall {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
@@ -39,9 +39,9 @@ export interface ToolProtocol {
|
|
|
39
39
|
/** Rewrite a tool call before execution (e.g., unwrap meta-tool). */
|
|
40
40
|
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
41
41
|
/** Record the assistant turn in conversation state. */
|
|
42
|
-
recordAssistant(conv:
|
|
42
|
+
recordAssistant(conv: LiveView, text: string, toolCalls: PendingToolCall[], extras?: Record<string, unknown>): void;
|
|
43
43
|
/** Record all tool results for a batch as conversation messages. */
|
|
44
|
-
recordResults(conv:
|
|
44
|
+
recordResults(conv: LiveView, results: ToolResult[]): void;
|
|
45
45
|
/** Create a stream filter for stripping tool calls from display. null = pass-through. */
|
|
46
46
|
createStreamFilter(toolNames: string[]): StreamFilter | null;
|
|
47
47
|
/**
|
|
@@ -57,8 +57,8 @@ export declare class ApiToolProtocol implements ToolProtocol {
|
|
|
57
57
|
getToolPrompt(): string;
|
|
58
58
|
extractToolCalls(_text: string, streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
59
59
|
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
60
|
-
recordAssistant(conv:
|
|
61
|
-
recordResults(conv:
|
|
60
|
+
recordAssistant(conv: LiveView, text: string, toolCalls: PendingToolCall[], extras?: Record<string, unknown>): void;
|
|
61
|
+
recordResults(conv: LiveView, results: ToolResult[]): void;
|
|
62
62
|
createStreamFilter(): null;
|
|
63
63
|
}
|
|
64
64
|
export declare class InlineToolProtocol implements ToolProtocol {
|
|
@@ -68,8 +68,8 @@ export declare class InlineToolProtocol implements ToolProtocol {
|
|
|
68
68
|
getToolPrompt(tools: ToolDefinition[]): string;
|
|
69
69
|
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
70
70
|
extractToolCalls(text: string, _streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
71
|
-
recordAssistant(conv:
|
|
72
|
-
recordResults(conv:
|
|
71
|
+
recordAssistant(conv: LiveView, text: string, _toolCalls: PendingToolCall[], extras?: Record<string, unknown>): void;
|
|
72
|
+
recordResults(conv: LiveView, results: ToolResult[]): void;
|
|
73
73
|
createStreamFilter(_toolNames: string[]): StreamFilter;
|
|
74
74
|
}
|
|
75
75
|
export declare class DeferredToolProtocol implements ToolProtocol {
|
|
@@ -82,8 +82,8 @@ export declare class DeferredToolProtocol implements ToolProtocol {
|
|
|
82
82
|
getToolPrompt(): string;
|
|
83
83
|
extractToolCalls(_text: string, streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
84
84
|
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
85
|
-
recordAssistant(conv:
|
|
86
|
-
recordResults(conv:
|
|
85
|
+
recordAssistant(conv: LiveView, text: string, toolCalls: PendingToolCall[], extras?: Record<string, unknown>): void;
|
|
86
|
+
recordResults(conv: LiveView, results: ToolResult[]): void;
|
|
87
87
|
createStreamFilter(): null;
|
|
88
88
|
}
|
|
89
89
|
export declare class DeferredLookupProtocol implements ToolProtocol {
|
|
@@ -97,8 +97,8 @@ export declare class DeferredLookupProtocol implements ToolProtocol {
|
|
|
97
97
|
getToolPrompt(): string;
|
|
98
98
|
extractToolCalls(_text: string, streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
99
99
|
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
100
|
-
recordAssistant(conv:
|
|
101
|
-
recordResults(conv:
|
|
100
|
+
recordAssistant(conv: LiveView, text: string, toolCalls: PendingToolCall[], extras?: Record<string, unknown>): void;
|
|
101
|
+
recordResults(conv: LiveView, results: ToolResult[]): void;
|
|
102
102
|
createStreamFilter(): null;
|
|
103
103
|
getProtocolTools(): ToolDefinition[];
|
|
104
104
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/** Bootstrap a throwaway core to enumerate provider ids extensions
|
|
2
2
|
* would register, so `auth list` shows ids the user hasn't keyed yet. */
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import { createCore } from "../../core/index.js";
|
|
4
5
|
import { activateAgent } from "../../agent/index.js";
|
|
5
6
|
import { loadExtensions } from "../../core/extension-loader.js";
|
|
6
7
|
import { loadBuiltinExtensions } from "../../extensions/index.js";
|
|
7
|
-
import { getSettings } from "../../core/settings.js";
|
|
8
|
+
import { CONFIG_DIR, getSettings } from "../../core/settings.js";
|
|
9
|
+
const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
10
|
+
const BARE_IMPORT_RE = /Cannot find (?:package|module) ['"]agent-sh\/[^'"]+['"]/;
|
|
8
11
|
let cached = null;
|
|
9
12
|
export async function discoverExtensionProviders() {
|
|
10
13
|
if (cached)
|
|
11
14
|
return cached;
|
|
12
15
|
const core = createCore({});
|
|
16
|
+
const errors = [];
|
|
17
|
+
core.bus.on("ui:error", ({ message }) => { errors.push(message); });
|
|
13
18
|
try {
|
|
14
19
|
const ctx = core.extensionContext({ quit: () => { } });
|
|
15
20
|
activateAgent(ctx);
|
|
@@ -17,6 +22,18 @@ export async function discoverExtensionProviders() {
|
|
|
17
22
|
await loadExtensions(ctx).catch(() => { });
|
|
18
23
|
const { providers } = core.bus.emitPipe("agent:providers", { providers: [] });
|
|
19
24
|
cached = providers.map((p) => ({ id: p.id, noAuth: p.noAuth }));
|
|
25
|
+
if (errors.length > 0) {
|
|
26
|
+
process.stderr.write(`\n[agent-sh] extension load errors during provider discovery:\n`);
|
|
27
|
+
for (const msg of errors) {
|
|
28
|
+
process.stderr.write(` - ${msg}\n`);
|
|
29
|
+
if (BARE_IMPORT_RE.test(msg) && msg.includes(EXT_DIR)) {
|
|
30
|
+
process.stderr.write(` ↳ Single-file extensions can't runtime-import agent-sh modules from ${EXT_DIR}.\n` +
|
|
31
|
+
` Use ctx.call(...) for runtime needs, or convert to a directory extension\n` +
|
|
32
|
+
` with its own package.json + node_modules.\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
process.stderr.write(`\n`);
|
|
36
|
+
}
|
|
20
37
|
return cached;
|
|
21
38
|
}
|
|
22
39
|
finally {
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { activateAgent } from "../agent/index.js";
|
|
|
4
4
|
import { createCore } from "../core/index.js";
|
|
5
5
|
import { palette as p } from "../utils/palette.js";
|
|
6
6
|
import { loadBuiltinExtensions } from "../extensions/index.js";
|
|
7
|
+
import activateRollingHistory from "../agent/extensions/rolling-history/index.js";
|
|
7
8
|
import { loadExtensions } from "../core/extension-loader.js";
|
|
8
9
|
import { getSettings } from "../core/settings.js";
|
|
9
10
|
import { dispatchSubcommand } from "./subcommands.js";
|
|
@@ -95,7 +96,9 @@ async function main() {
|
|
|
95
96
|
registerShellHandlers(extCtx);
|
|
96
97
|
activateAgent(extCtx);
|
|
97
98
|
// Load before spawning the shell so PS1 lands below the banner.
|
|
98
|
-
|
|
99
|
+
const settings = getSettings();
|
|
100
|
+
await loadBuiltinExtensions(extCtx, settings.disabledBuiltins);
|
|
101
|
+
activateRollingHistory(extCtx);
|
|
99
102
|
const loadExtensionsTimeoutMs = 10000;
|
|
100
103
|
let loadedExtensions = [];
|
|
101
104
|
await Promise.race([
|
|
@@ -124,7 +127,6 @@ async function main() {
|
|
|
124
127
|
hint);
|
|
125
128
|
process.exit(1);
|
|
126
129
|
}
|
|
127
|
-
const settings = getSettings();
|
|
128
130
|
if (settings.startupBanner !== false) {
|
|
129
131
|
const termW = process.stdout.columns || 80;
|
|
130
132
|
const bannerW = Math.min(termW, 60);
|
package/dist/core/index.d.ts
CHANGED
|
@@ -20,7 +20,6 @@ export type { ColorPalette } from "../utils/palette.js";
|
|
|
20
20
|
export type { AgentBackend, ToolDefinition, ImageContent } from "../agent/types.js";
|
|
21
21
|
export { runSubagent, type SubagentOptions } from "../agent/subagent.js";
|
|
22
22
|
export { LlmClient } from "../agent/llm-client.js";
|
|
23
|
-
export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "../agent/history-file.js";
|
|
24
23
|
export type { NuclearEntry } from "../agent/nuclear-form.js";
|
|
25
24
|
export { compileSearchRegex, matchEntry, formatNuclearLine } from "../agent/nuclear-form.js";
|
|
26
25
|
export interface AgentShellCore {
|
package/dist/core/index.js
CHANGED
|
@@ -19,7 +19,6 @@ export { EventBus } from "./event-bus.js";
|
|
|
19
19
|
export { palette, setPalette, resetPalette } from "../utils/palette.js";
|
|
20
20
|
export { runSubagent } from "../agent/subagent.js";
|
|
21
21
|
export { LlmClient } from "../agent/llm-client.js";
|
|
22
|
-
export { HistoryFile, InMemoryHistory, NoopHistory } from "../agent/history-file.js";
|
|
23
22
|
export { compileSearchRegex, matchEntry, formatNuclearLine } from "../agent/nuclear-form.js";
|
|
24
23
|
export function createCore(config) {
|
|
25
24
|
const bus = new EventBus();
|
package/dist/core/settings.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface Settings {
|
|
|
65
65
|
* that boot agent-sh as a library against a specific working tree).
|
|
66
66
|
*/
|
|
67
67
|
historyFilePath?: string;
|
|
68
|
-
|
|
68
|
+
autoCompact?: boolean;
|
|
69
69
|
autoCompactThreshold?: number;
|
|
70
70
|
/** Max command output lines shown inline in TUI. */
|
|
71
71
|
maxCommandOutputLines?: number;
|
|
@@ -116,8 +116,12 @@ export interface Settings {
|
|
|
116
116
|
disabledExtensions?: string[];
|
|
117
117
|
}
|
|
118
118
|
declare const DEFAULTS: Required<Settings>;
|
|
119
|
+
export type SettingSource = "session" | "env" | "file" | "default";
|
|
119
120
|
/** Load settings from disk (cached after first call). */
|
|
120
121
|
export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
122
|
+
export declare function setSessionOverlay(patch: Partial<Settings>): void;
|
|
123
|
+
export declare function clearSessionOverlay(...keys: (keyof Settings)[]): void;
|
|
124
|
+
export declare function getSettingSource(key: keyof Settings): SettingSource;
|
|
121
125
|
/**
|
|
122
126
|
* Get settings for an extension, namespaced under its key in settings.json.
|
|
123
127
|
*
|
package/dist/core/settings.js
CHANGED
|
@@ -28,6 +28,7 @@ const DEFAULTS = {
|
|
|
28
28
|
historyMaxBytes: 104857600, // 100MB — history is only accessed via search/expand, never loaded wholesale
|
|
29
29
|
historyStartupEntries: 100,
|
|
30
30
|
historyFilePath: undefined,
|
|
31
|
+
autoCompact: true,
|
|
31
32
|
autoCompactThreshold: 0.5,
|
|
32
33
|
maxCommandOutputLines: 3,
|
|
33
34
|
readOutputMaxLines: 10,
|
|
@@ -41,6 +42,42 @@ const DEFAULTS = {
|
|
|
41
42
|
disabledExtensions: [],
|
|
42
43
|
};
|
|
43
44
|
let cached = null;
|
|
45
|
+
let envOverrides = null;
|
|
46
|
+
let sessionOverlay = {};
|
|
47
|
+
function parseBoolEnv(raw, key) {
|
|
48
|
+
if (raw === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
const v = raw.trim().toLowerCase();
|
|
51
|
+
if (v === "on" || v === "true" || v === "1")
|
|
52
|
+
return true;
|
|
53
|
+
if (v === "off" || v === "false" || v === "0")
|
|
54
|
+
return false;
|
|
55
|
+
console.error(`[agent-sh] Warning: ${key}="${raw}" is not a boolean (off|on|true|false|0|1); ignoring.`);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function parseUnitFloatEnv(raw, key) {
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const n = Number(raw);
|
|
62
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
63
|
+
console.error(`[agent-sh] Warning: ${key}="${raw}" is not a number in [0, 1]; ignoring.`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
function loadEnvOverrides() {
|
|
69
|
+
if (envOverrides)
|
|
70
|
+
return envOverrides;
|
|
71
|
+
const out = {};
|
|
72
|
+
const ac = parseBoolEnv(process.env.AGENT_SH_AUTO_COMPACT, "AGENT_SH_AUTO_COMPACT");
|
|
73
|
+
if (ac !== undefined)
|
|
74
|
+
out.autoCompact = ac;
|
|
75
|
+
const th = parseUnitFloatEnv(process.env.AGENT_SH_AUTO_COMPACT_THRESHOLD, "AGENT_SH_AUTO_COMPACT_THRESHOLD");
|
|
76
|
+
if (th !== undefined)
|
|
77
|
+
out.autoCompactThreshold = th;
|
|
78
|
+
envOverrides = out;
|
|
79
|
+
return envOverrides;
|
|
80
|
+
}
|
|
44
81
|
/** Load settings from disk (cached after first call). */
|
|
45
82
|
export function getSettings() {
|
|
46
83
|
if (!cached) {
|
|
@@ -55,7 +92,29 @@ export function getSettings() {
|
|
|
55
92
|
cached = {};
|
|
56
93
|
}
|
|
57
94
|
}
|
|
58
|
-
return { ...DEFAULTS, ...cached };
|
|
95
|
+
return { ...DEFAULTS, ...cached, ...loadEnvOverrides(), ...sessionOverlay };
|
|
96
|
+
}
|
|
97
|
+
export function setSessionOverlay(patch) {
|
|
98
|
+
sessionOverlay = { ...sessionOverlay, ...patch };
|
|
99
|
+
}
|
|
100
|
+
export function clearSessionOverlay(...keys) {
|
|
101
|
+
if (keys.length === 0) {
|
|
102
|
+
sessionOverlay = {};
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const next = { ...sessionOverlay };
|
|
106
|
+
for (const k of keys)
|
|
107
|
+
delete next[k];
|
|
108
|
+
sessionOverlay = next;
|
|
109
|
+
}
|
|
110
|
+
export function getSettingSource(key) {
|
|
111
|
+
if (key in sessionOverlay)
|
|
112
|
+
return "session";
|
|
113
|
+
if (key in loadEnvOverrides())
|
|
114
|
+
return "env";
|
|
115
|
+
if (cached && key in cached)
|
|
116
|
+
return "file";
|
|
117
|
+
return "default";
|
|
59
118
|
}
|
|
60
119
|
/**
|
|
61
120
|
* Get settings for an extension, namespaced under its key in settings.json.
|
|
@@ -78,6 +137,8 @@ export function getExtensionSettings(namespace, defaults) {
|
|
|
78
137
|
/** Reset cached settings (for testing or after external edit). */
|
|
79
138
|
export function reloadSettings() {
|
|
80
139
|
cached = null;
|
|
140
|
+
envOverrides = null;
|
|
141
|
+
sessionOverlay = {};
|
|
81
142
|
}
|
|
82
143
|
/**
|
|
83
144
|
* Deep-merge a patch into ~/.agent-sh/settings.json on disk.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Module-owned built-ins activate inline:
|
|
4
4
|
* shell-context, tui-renderer → registerShellHandlers (src/shell/)
|
|
5
5
|
* ash (a specific backend) → activateAgent (src/agent/)
|
|
6
|
+
* rolling-history → activateRollingHistory (src/cli/)
|
|
6
7
|
* backend registry → createCore (src/core/)
|
|
7
8
|
*/
|
|
8
9
|
import type { ExtensionContext } from "../shell/host-types.js";
|
package/dist/shell/events.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export class InputHandler {
|
|
|
35
35
|
if (this.activeMode)
|
|
36
36
|
this.drawPrompt();
|
|
37
37
|
});
|
|
38
|
+
this.bus.on("input:redraw", () => {
|
|
39
|
+
if (this.activeMode)
|
|
40
|
+
this.renderModeInput();
|
|
41
|
+
});
|
|
38
42
|
this.bus.on("input-mode:register", (config) => {
|
|
39
43
|
this.registerMode(config);
|
|
40
44
|
});
|