agent-sh 0.14.1 → 0.14.3
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 +1 -1
- package/dist/agent/agent-loop.js +42 -31
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +20 -3
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/host-types.d.ts +3 -0
- package/dist/agent/index.js +2 -1
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/subagent.js +5 -1
- package/dist/agent/tool-protocol.d.ts +2 -2
- package/dist/agent/tool-protocol.js +5 -4
- package/dist/agent/tools/glob.d.ts +1 -1
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.d.ts +1 -1
- package/dist/agent/tools/grep.js +4 -2
- package/dist/agent/tools/ls.d.ts +1 -1
- package/dist/agent/tools/ls.js +4 -2
- package/dist/agent/tools/read-file.d.ts +1 -1
- package/dist/agent/tools/read-file.js +30 -2
- package/dist/agent/types.d.ts +11 -1
- package/dist/agent/types.js +6 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/settings.d.ts +3 -0
- package/dist/core/settings.js +2 -2
- package/dist/shell/events.d.ts +2 -0
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/output-parser.d.ts +11 -22
- package/dist/shell/output-parser.js +16 -34
- package/dist/shell/shell-context.d.ts +3 -6
- package/dist/shell/shell-context.js +15 -7
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +18 -30
- package/dist/shell/strategies/types.d.ts +6 -0
- package/dist/shell/strategies/zsh.js +7 -0
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/examples/extensions/ash-scheme/index.ts +2170 -0
- package/examples/extensions/ash-scheme/package.json +11 -0
- package/examples/extensions/ash-scheme-render.ts +58 -0
- package/examples/extensions/ashi/README.md +36 -26
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +1 -0
- package/examples/extensions/ashi/src/cli.ts +53 -11
- package/examples/extensions/ashi/src/commands.ts +2 -20
- package/examples/extensions/ashi/src/compaction.ts +25 -96
- package/examples/extensions/ashi/src/components.ts +64 -166
- package/examples/extensions/ashi/src/default-schema-renderers.ts +232 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +355 -118
- package/examples/extensions/ashi/src/hooks.ts +47 -63
- package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
- package/examples/extensions/ashi/src/schema.ts +386 -0
- package/examples/extensions/ashi/src/session-store.ts +115 -17
- package/examples/extensions/ashi/src/shell-mode.ts +52 -0
- package/examples/extensions/ashi/src/status-footer.ts +41 -6
- package/examples/extensions/ashi/src/theme.ts +2 -1
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +2 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -0
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +16 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
|
@@ -39,12 +39,30 @@ export interface CompactionEntry {
|
|
|
39
39
|
id: string;
|
|
40
40
|
parentId: string;
|
|
41
41
|
timestamp: number;
|
|
42
|
-
summary
|
|
42
|
+
summary?: string;
|
|
43
43
|
firstKeptId: string;
|
|
44
44
|
tokensBefore: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/** Omitted from buildMessages — the agent already saw it via <shell_events>
|
|
48
|
+
* (or didn't, if private). The frontend replays it for scrollback fidelity. */
|
|
49
|
+
export interface ShellExchangeEntry {
|
|
50
|
+
type: "shell-exchange";
|
|
51
|
+
id: string;
|
|
52
|
+
parentId: string;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
command: string;
|
|
55
|
+
output: string;
|
|
56
|
+
exitCode: number | null;
|
|
57
|
+
cwd?: string;
|
|
58
|
+
private?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type SessionEntry =
|
|
62
|
+
| SessionHeaderEntry
|
|
63
|
+
| MessageEntry
|
|
64
|
+
| CompactionEntry
|
|
65
|
+
| ShellExchangeEntry;
|
|
48
66
|
|
|
49
67
|
export interface SessionMeta {
|
|
50
68
|
name?: string;
|
|
@@ -55,8 +73,63 @@ export function newEntryId(): string {
|
|
|
55
73
|
return crypto.randomBytes(4).toString("hex");
|
|
56
74
|
}
|
|
57
75
|
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
function extractText(content: unknown): string {
|
|
77
|
+
if (typeof content === "string") return content;
|
|
78
|
+
if (Array.isArray(content)) {
|
|
79
|
+
return content.map((p) => {
|
|
80
|
+
if (typeof p === "string") return p;
|
|
81
|
+
const part = p as { text?: string; content?: string };
|
|
82
|
+
return part?.text ?? part?.content ?? "";
|
|
83
|
+
}).join(" ");
|
|
84
|
+
}
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function snippet(text: string, max: number): string {
|
|
89
|
+
const cleaned = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
90
|
+
if (cleaned.length <= max) return cleaned || "(empty)";
|
|
91
|
+
return cleaned.slice(0, max) + "…";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function summarizeMessage(m: AgentMessage): string {
|
|
95
|
+
const role = m.role ?? "?";
|
|
96
|
+
if (role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
97
|
+
const tools = m.tool_calls.map((tc) => {
|
|
98
|
+
const name = tc.function?.name ?? "tool";
|
|
99
|
+
const args = tc.function?.arguments;
|
|
100
|
+
return args ? `${name}(${snippet(args, 200)})` : name;
|
|
101
|
+
}).join(", ");
|
|
102
|
+
const text = extractText(m.content);
|
|
103
|
+
const prefix = text ? `${snippet(text, 400)} → ` : "";
|
|
104
|
+
return `assistant: ${prefix}called ${tools}`;
|
|
105
|
+
}
|
|
106
|
+
if (role === "tool") {
|
|
107
|
+
const text = typeof m.content === "string" ? m.content : extractText(m.content);
|
|
108
|
+
const isErr = /^error\b|: error\b/i.test(text.slice(0, 200));
|
|
109
|
+
return `tool result: ${snippet(text, isErr ? 1000 : 400)}`;
|
|
110
|
+
}
|
|
111
|
+
if (role === "user") {
|
|
112
|
+
return `user: ${snippet(extractText(m.content), 1000)}`;
|
|
113
|
+
}
|
|
114
|
+
return `${role}: ${snippet(extractText(m.content), 500)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** For displayed user text. Loops because both wrappers can stack at the head. */
|
|
118
|
+
export function stripContextWrappers(content: string): string {
|
|
119
|
+
let out = content;
|
|
120
|
+
for (;;) {
|
|
121
|
+
const next = out.replace(/^\s*<(query_context|dynamic_context)>[\s\S]*?<\/\1>\s*/, "");
|
|
122
|
+
if (next === out) return out;
|
|
123
|
+
out = next;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function renderEvictedSummary(evicted: AgentMessage[]): string {
|
|
128
|
+
const lines = evicted.map((m) => `- ${summarizeMessage(m)}`);
|
|
129
|
+
return `${lines.length} message(s) elided\n${lines.join("\n")}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Tree is implicit via parentId pointers; entries are kept in memory after load. */
|
|
60
133
|
export class SessionStore {
|
|
61
134
|
private entriesPath: string;
|
|
62
135
|
private leafPath: string;
|
|
@@ -124,9 +197,6 @@ export class SessionStore {
|
|
|
124
197
|
this.persistMeta();
|
|
125
198
|
}
|
|
126
199
|
|
|
127
|
-
/** Append messages as a chain of MessageEntry, each parented at the
|
|
128
|
-
* previously appended id (starting from current leaf). Returns the new
|
|
129
|
-
* entry ids in order. */
|
|
130
200
|
async appendMessages(messages: AgentMessage[]): Promise<string[]> {
|
|
131
201
|
if (messages.length === 0) return [];
|
|
132
202
|
this.flushHeader();
|
|
@@ -152,7 +222,33 @@ export class SessionStore {
|
|
|
152
222
|
return newIds;
|
|
153
223
|
}
|
|
154
224
|
|
|
155
|
-
async
|
|
225
|
+
async appendShellExchange(e: {
|
|
226
|
+
command: string;
|
|
227
|
+
output: string;
|
|
228
|
+
exitCode: number | null;
|
|
229
|
+
cwd?: string;
|
|
230
|
+
private?: boolean;
|
|
231
|
+
}): Promise<string> {
|
|
232
|
+
this.flushHeader();
|
|
233
|
+
const entry: ShellExchangeEntry = {
|
|
234
|
+
type: "shell-exchange",
|
|
235
|
+
id: newEntryId(),
|
|
236
|
+
parentId: this.activeLeaf,
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
command: e.command,
|
|
239
|
+
output: e.output,
|
|
240
|
+
exitCode: e.exitCode,
|
|
241
|
+
...(e.cwd !== undefined ? { cwd: e.cwd } : {}),
|
|
242
|
+
...(e.private ? { private: true } : {}),
|
|
243
|
+
};
|
|
244
|
+
this.entries.set(entry.id, entry);
|
|
245
|
+
this.activeLeaf = entry.id;
|
|
246
|
+
await fsp.appendFile(this.entriesPath, JSON.stringify(entry) + "\n");
|
|
247
|
+
this.persistLeaf();
|
|
248
|
+
return entry.id;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async appendCompaction(firstKeptId: string, tokensBefore: number, summary?: string): Promise<string> {
|
|
156
252
|
if (!this.entries.has(firstKeptId)) throw new Error(`firstKeptId unknown: ${firstKeptId}`);
|
|
157
253
|
this.flushHeader();
|
|
158
254
|
const e: CompactionEntry = {
|
|
@@ -160,9 +256,9 @@ export class SessionStore {
|
|
|
160
256
|
id: newEntryId(),
|
|
161
257
|
parentId: this.activeLeaf,
|
|
162
258
|
timestamp: Date.now(),
|
|
163
|
-
summary,
|
|
164
259
|
firstKeptId,
|
|
165
260
|
tokensBefore,
|
|
261
|
+
...(summary !== undefined ? { summary } : {}),
|
|
166
262
|
};
|
|
167
263
|
this.entries.set(e.id, e);
|
|
168
264
|
this.activeLeaf = e.id;
|
|
@@ -171,7 +267,7 @@ export class SessionStore {
|
|
|
171
267
|
return e.id;
|
|
172
268
|
}
|
|
173
269
|
|
|
174
|
-
/**
|
|
270
|
+
/** Oldest-first walk from leaf to root. */
|
|
175
271
|
getBranch(leafId: string = this.activeLeaf): SessionEntry[] {
|
|
176
272
|
const out: SessionEntry[] = [];
|
|
177
273
|
const seen = new Set<string>();
|
|
@@ -186,9 +282,7 @@ export class SessionStore {
|
|
|
186
282
|
return out.reverse();
|
|
187
283
|
}
|
|
188
284
|
|
|
189
|
-
/**
|
|
190
|
-
* latest compaction on the branch (summary + kept tail). Mirrors pi's
|
|
191
|
-
* buildSessionContext. */
|
|
285
|
+
/** Honors the latest compaction on the branch (summary + kept tail). */
|
|
192
286
|
buildMessages(leafId: string = this.activeLeaf): AgentMessage[] {
|
|
193
287
|
const branch = this.getBranch(leafId);
|
|
194
288
|
let compactionIdx = -1;
|
|
@@ -203,9 +297,14 @@ export class SessionStore {
|
|
|
203
297
|
const c = branch[compactionIdx] as CompactionEntry;
|
|
204
298
|
const firstKeptIdx = branch.findIndex((e) => e.id === c.firstKeptId);
|
|
205
299
|
const keepFrom = firstKeptIdx >= 0 ? firstKeptIdx : 0;
|
|
300
|
+
const summary = c.summary ?? renderEvictedSummary(
|
|
301
|
+
branch.slice(0, keepFrom)
|
|
302
|
+
.filter((e): e is MessageEntry => e.type === "message")
|
|
303
|
+
.map((e) => e.message),
|
|
304
|
+
);
|
|
206
305
|
const out: AgentMessage[] = [{
|
|
207
306
|
role: "user",
|
|
208
|
-
content: `[Compacted conversation summary]\n${
|
|
307
|
+
content: `[Compacted conversation summary]\n${summary}`,
|
|
209
308
|
}];
|
|
210
309
|
for (let i = keepFrom; i < branch.length; i++) {
|
|
211
310
|
const e = branch[i]!;
|
|
@@ -214,12 +313,11 @@ export class SessionStore {
|
|
|
214
313
|
return out;
|
|
215
314
|
}
|
|
216
315
|
|
|
217
|
-
/** A short, human-friendly preview for picker rows. Uses the first user
|
|
218
|
-
* message's text when available, else the session id. */
|
|
219
316
|
getPreview(): string {
|
|
220
317
|
for (const e of this.entries.values()) {
|
|
221
318
|
if (e.type === "message" && e.message.role === "user") {
|
|
222
|
-
const
|
|
319
|
+
const raw = typeof e.message.content === "string" ? e.message.content : "";
|
|
320
|
+
const txt = stripContextWrappers(raw);
|
|
223
321
|
if (txt) return txt.slice(0, 80);
|
|
224
322
|
}
|
|
225
323
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface ChangeHandlerResult {
|
|
2
|
+
mode: boolean;
|
|
3
|
+
/** When set, caller must `editor.setText(replaceText)` to strip the `!`. */
|
|
4
|
+
replaceText?: string;
|
|
5
|
+
/** Whether the next submit (if in shell mode) is marked private. */
|
|
6
|
+
pendingPrivate: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Strips `!` (entry), `!!` (entry + private), in-mode `!` (upgrade to private).
|
|
10
|
+
* Shell mode is sticky — exit only via the Backspace-on-empty intercept;
|
|
11
|
+
* auto-exit on empty text would fire during pi-tui's pre-emptive onChange("")
|
|
12
|
+
* inside Editor.submitValue() and misroute the submit. pendingPrivate is
|
|
13
|
+
* sticky for the same reason. */
|
|
14
|
+
export function deriveChangeHandlerResult(
|
|
15
|
+
mode: boolean,
|
|
16
|
+
pendingPrivate: boolean,
|
|
17
|
+
text: string,
|
|
18
|
+
): ChangeHandlerResult {
|
|
19
|
+
if (!mode && text.startsWith("!!")) {
|
|
20
|
+
return { mode: true, replaceText: text.slice(2), pendingPrivate: true };
|
|
21
|
+
}
|
|
22
|
+
if (!mode && text.startsWith("!")) {
|
|
23
|
+
return { mode: true, replaceText: text.slice(1), pendingPrivate: false };
|
|
24
|
+
}
|
|
25
|
+
if (mode && text.startsWith("!")) {
|
|
26
|
+
return { mode: true, replaceText: text.slice(1), pendingPrivate: true };
|
|
27
|
+
}
|
|
28
|
+
return { mode, pendingPrivate: pendingPrivate && mode };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SubmitAction =
|
|
32
|
+
| { kind: "noop" }
|
|
33
|
+
| { kind: "shell"; line: string; private: boolean }
|
|
34
|
+
| { kind: "command"; name: string; args: string }
|
|
35
|
+
| { kind: "agent"; query: string };
|
|
36
|
+
|
|
37
|
+
export function classifySubmit(
|
|
38
|
+
text: string,
|
|
39
|
+
shellMode: boolean,
|
|
40
|
+
pendingPrivate: boolean,
|
|
41
|
+
): SubmitAction {
|
|
42
|
+
const query = text.trim();
|
|
43
|
+
if (!query) return { kind: "noop" };
|
|
44
|
+
if (shellMode) return { kind: "shell", line: query, private: pendingPrivate };
|
|
45
|
+
if (query.startsWith("/")) {
|
|
46
|
+
const sp = query.indexOf(" ");
|
|
47
|
+
const name = sp === -1 ? query : query.slice(0, sp);
|
|
48
|
+
const args = sp === -1 ? "" : query.slice(sp + 1).trim();
|
|
49
|
+
return { kind: "command", name, args };
|
|
50
|
+
}
|
|
51
|
+
return { kind: "agent", query };
|
|
52
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { Container, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import { theme } from "./theme.js";
|
|
3
4
|
|
|
4
5
|
interface StatusFields {
|
|
@@ -11,11 +12,13 @@ interface StatusFields {
|
|
|
11
12
|
tokens?: number;
|
|
12
13
|
compactions?: number;
|
|
13
14
|
thinking?: string;
|
|
15
|
+
shellMode?: "off" | "on" | "private";
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export class StatusFooter extends Container {
|
|
17
19
|
private text: Text;
|
|
18
20
|
private fields: StatusFields = {};
|
|
21
|
+
private lastWidth = 0;
|
|
19
22
|
|
|
20
23
|
constructor() {
|
|
21
24
|
super();
|
|
@@ -25,10 +28,41 @@ export class StatusFooter extends Container {
|
|
|
25
28
|
|
|
26
29
|
update(patch: Partial<StatusFields>): void {
|
|
27
30
|
this.fields = { ...this.fields, ...patch };
|
|
28
|
-
this.repaint();
|
|
31
|
+
this.repaint(this.lastWidth);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
render(width: number): string[] {
|
|
35
|
+
if (width !== this.lastWidth) {
|
|
36
|
+
this.lastWidth = width;
|
|
37
|
+
this.repaint(width);
|
|
38
|
+
}
|
|
39
|
+
return super.render(width);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private repaint(width: number): void {
|
|
43
|
+
const contentWidth = width > 0 ? Math.max(1, width - 2) : 0;
|
|
44
|
+
const right = this.buildRight();
|
|
45
|
+
const rightWidth = visibleWidth(right);
|
|
46
|
+
const join = (left: string): string => {
|
|
47
|
+
if (!right) return left;
|
|
48
|
+
const leftWidth = visibleWidth(left);
|
|
49
|
+
const gap = Math.max(1, contentWidth - leftWidth - rightWidth);
|
|
50
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
51
|
+
};
|
|
52
|
+
const full = this.buildLine("full");
|
|
53
|
+
const fullFits = contentWidth === 0
|
|
54
|
+
|| visibleWidth(full) + (right ? rightWidth + 1 : 0) <= contentWidth;
|
|
55
|
+
this.text.setText(fullFits ? join(full) : join(this.buildLine("basename")));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private buildRight(): string {
|
|
59
|
+
const mode = this.fields.shellMode;
|
|
60
|
+
if (mode === "on") return theme.fg("bashMode", "▸ shell");
|
|
61
|
+
if (mode === "private") return theme.fg("bashModePrivate", "▸ shell · private");
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private buildLine(cwdMode: "full" | "basename"): string {
|
|
32
66
|
const { model, provider, contextWindow, cwd, branch, leaf, tokens, compactions, thinking } = this.fields;
|
|
33
67
|
const sep = theme.fg("dim", " | ");
|
|
34
68
|
const parts: string[] = [];
|
|
@@ -39,7 +73,7 @@ export class StatusFooter extends Container {
|
|
|
39
73
|
} else if (provider) {
|
|
40
74
|
parts.push(theme.fg("muted", `@${provider}`));
|
|
41
75
|
}
|
|
42
|
-
if (cwd) parts.push(theme.fg("muted",
|
|
76
|
+
if (cwd) parts.push(theme.fg("muted", formatCwd(cwd, cwdMode)));
|
|
43
77
|
if (branch) parts.push(theme.fg("muted", `⎇ ${branch}`));
|
|
44
78
|
if (leaf != null && leaf > 0) parts.push(theme.fg("muted", `#${leaf}`));
|
|
45
79
|
if (tokens != null) {
|
|
@@ -48,11 +82,12 @@ export class StatusFooter extends Container {
|
|
|
48
82
|
parts.push(`${theme.fg("muted", tokStr)}${pct}`);
|
|
49
83
|
}
|
|
50
84
|
if (compactions && compactions > 0) parts.push(theme.fg("muted", `⊟ ${compactions}`));
|
|
51
|
-
|
|
85
|
+
return parts.length === 0 ? "" : parts.join(sep);
|
|
52
86
|
}
|
|
53
87
|
}
|
|
54
88
|
|
|
55
|
-
function
|
|
89
|
+
function formatCwd(cwd: string, mode: "full" | "basename"): string {
|
|
90
|
+
if (mode === "basename") return basename(cwd) || cwd;
|
|
56
91
|
const home = process.env.HOME;
|
|
57
92
|
if (home && cwd.startsWith(`${home}/`)) return `~/${cwd.slice(home.length + 1)}`;
|
|
58
93
|
if (home && cwd === home) return "~";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces ashi's default deterministic compaction summary with an
|
|
3
|
+
* LLM-generated structured one. Advises `ashi:compact:build-summary`;
|
|
4
|
+
* orchestration stays in ashi. Falls back to deterministic when the LLM
|
|
5
|
+
* is unavailable or the call fails.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentContext } from "agent-sh/types";
|
|
8
|
+
|
|
9
|
+
interface AgentMessage {
|
|
10
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
11
|
+
content?: unknown;
|
|
12
|
+
tool_calls?: { function?: { name?: string; arguments?: string } }[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SUMMARY_PROMPT = `You are compacting a coding-agent conversation so the agent can continue with limited context.
|
|
16
|
+
|
|
17
|
+
Produce a Markdown summary using EXACTLY this structure:
|
|
18
|
+
|
|
19
|
+
## Goal
|
|
20
|
+
[What the user is trying to accomplish, one or two sentences]
|
|
21
|
+
|
|
22
|
+
## Constraints & Preferences
|
|
23
|
+
- [Bulleted user requirements / preferences expressed so far]
|
|
24
|
+
|
|
25
|
+
## Progress
|
|
26
|
+
### Done
|
|
27
|
+
- [x] [Completed work]
|
|
28
|
+
|
|
29
|
+
### In Progress
|
|
30
|
+
- [ ] [Active work and current sub-goal]
|
|
31
|
+
|
|
32
|
+
### Blocked
|
|
33
|
+
- [Issues, or "None"]
|
|
34
|
+
|
|
35
|
+
## Key Decisions
|
|
36
|
+
- **[Decision]**: [Rationale]
|
|
37
|
+
|
|
38
|
+
## Next Steps
|
|
39
|
+
1. [What should happen next]
|
|
40
|
+
|
|
41
|
+
## Critical Context
|
|
42
|
+
- [Specific paths, names, identifiers, or data the agent must remember]
|
|
43
|
+
|
|
44
|
+
Be concrete. Quote file paths, function names, error strings verbatim when relevant. Do not invent details that aren't in the conversation.`;
|
|
45
|
+
|
|
46
|
+
export default function activate(ctx: AgentContext): void {
|
|
47
|
+
ctx.advise(
|
|
48
|
+
"ashi:compact:build-summary",
|
|
49
|
+
async (next: (...a: unknown[]) => unknown, evicted: AgentMessage[]) => {
|
|
50
|
+
const llm = ctx.agent?.llm;
|
|
51
|
+
if (!llm?.available) return next(evicted);
|
|
52
|
+
try {
|
|
53
|
+
const summary = await llm.ask({
|
|
54
|
+
system: SUMMARY_PROMPT,
|
|
55
|
+
query: buildQuery(evicted),
|
|
56
|
+
maxTokens: 16384,
|
|
57
|
+
reasoningEffort: "low",
|
|
58
|
+
});
|
|
59
|
+
return summary.trim();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
ctx.bus.emit("ui:error", {
|
|
62
|
+
message: `ashi-compact-llm: LLM failed (${(e as Error).message}); falling back to deterministic summary`,
|
|
63
|
+
});
|
|
64
|
+
return next(evicted);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildQuery(messages: AgentMessage[]): string {
|
|
71
|
+
const lines: string[] = ["Conversation to summarize:"];
|
|
72
|
+
for (const m of messages) {
|
|
73
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
74
|
+
if (m.role === "user") lines.push(`[User]: ${text}`);
|
|
75
|
+
else if (m.role === "assistant") {
|
|
76
|
+
if (text) lines.push(`[Assistant]: ${text}`);
|
|
77
|
+
if (m.tool_calls) {
|
|
78
|
+
for (const t of m.tool_calls) {
|
|
79
|
+
const args = t.function?.arguments ?? "";
|
|
80
|
+
lines.push(`[Assistant tool call]: ${t.function?.name ?? "?"}(${truncate(args, 400)})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else if (m.role === "tool") {
|
|
84
|
+
lines.push(`[Tool result]: ${truncate(text, 2000)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function truncate(s: string, max: number): string {
|
|
91
|
+
if (s.length <= max) return s;
|
|
92
|
+
return s.slice(0, max) + `\n[…truncated ${s.length - max} chars…]`;
|
|
93
|
+
}
|
|
@@ -140,6 +140,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
140
140
|
const kind = toolKind(meta.name);
|
|
141
141
|
bus.emit("agent:tool-started", {
|
|
142
142
|
title: meta.name,
|
|
143
|
+
name: meta.name,
|
|
143
144
|
toolCallId: meta.id,
|
|
144
145
|
kind,
|
|
145
146
|
icon: toolIcon(meta.name),
|
|
@@ -176,6 +177,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
176
177
|
const kind = toolKind(b.name);
|
|
177
178
|
bus.emit("agent:tool-started", {
|
|
178
179
|
title: b.name,
|
|
180
|
+
name: b.name,
|
|
179
181
|
toolCallId: b.id,
|
|
180
182
|
kind,
|
|
181
183
|
icon: toolIcon(b.name),
|
|
@@ -147,6 +147,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
147
147
|
announcedTools.add(callID);
|
|
148
148
|
bus.emit("agent:tool-started", {
|
|
149
149
|
title: toolName,
|
|
150
|
+
name: toolName,
|
|
150
151
|
toolCallId: callID,
|
|
151
152
|
kind,
|
|
152
153
|
locations: toolLocations(state.input ?? {}),
|
|
@@ -290,6 +291,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
290
291
|
: req.questions.map((q, i) => `${q.header || `Q${i + 1}`}: ${q.question}`).join("; ");
|
|
291
292
|
bus.emit("agent:tool-started", {
|
|
292
293
|
title: "question",
|
|
294
|
+
name: "question",
|
|
293
295
|
toolCallId: callID,
|
|
294
296
|
kind: "execute",
|
|
295
297
|
displayDetail: detail,
|
|
@@ -355,6 +357,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
355
357
|
const summary = opts?.note ? `denied (${opts.note})` : `denied: ${detail}`;
|
|
356
358
|
bus.emit("agent:tool-started", {
|
|
357
359
|
title: "permission",
|
|
360
|
+
name: "permission",
|
|
358
361
|
toolCallId: callID,
|
|
359
362
|
kind: "execute",
|
|
360
363
|
displayDetail: detail,
|