agent-sh 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/agent/agent-loop.d.ts +2 -2
- package/dist/agent/agent-loop.js +106 -13
- package/dist/agent/conversation-state.d.ts +39 -9
- package/dist/agent/conversation-state.js +336 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +175 -0
- package/dist/agent/system-prompt.d.ts +2 -2
- package/dist/agent/system-prompt.js +25 -4
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/context-manager.d.ts +3 -2
- package/dist/context-manager.js +16 -111
- package/dist/core.js +30 -1
- package/dist/event-bus.d.ts +37 -0
- package/dist/extensions/overlay-agent.d.ts +14 -0
- package/dist/extensions/overlay-agent.js +147 -0
- package/dist/extensions/slash-commands.js +28 -0
- package/dist/extensions/terminal-buffer.d.ts +14 -0
- package/dist/extensions/terminal-buffer.js +125 -0
- package/dist/extensions/tui-renderer.js +122 -84
- package/dist/index.js +4 -0
- package/dist/input-handler.js +6 -1
- package/dist/output-parser.js +8 -0
- package/dist/settings.d.ts +19 -2
- package/dist/settings.js +21 -3
- package/dist/shell.d.ts +5 -0
- package/dist/shell.js +31 -2
- package/dist/token-budget.d.ts +13 -0
- package/dist/token-budget.js +50 -0
- package/dist/types.d.ts +13 -22
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/floating-panel.d.ts +227 -0
- package/dist/utils/floating-panel.js +807 -0
- package/dist/utils/line-editor.d.ts +9 -0
- package/dist/utils/line-editor.js +44 -0
- package/dist/utils/markdown.js +3 -3
- package/dist/utils/output-writer.d.ts +14 -0
- package/dist/utils/output-writer.js +16 -0
- package/dist/utils/terminal-buffer.d.ts +69 -0
- package/dist/utils/terminal-buffer.js +179 -0
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/overlay-agent.ts +70 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/terminal-buffer.ts +184 -0
- package/package.json +5 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// ── Tool classification ───────────────────────────────────────────
|
|
2
|
+
/** Read-only tools whose results are dropped at Tier 1→2 (agent can re-read). */
|
|
3
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
4
|
+
"read_file", "grep", "glob", "ls", "search",
|
|
5
|
+
]);
|
|
6
|
+
/** State-changing tools whose summaries are kept in nuclear memory. */
|
|
7
|
+
export const WRITE_TOOLS = new Set([
|
|
8
|
+
"write_file", "edit_file", "write", "edit", "patch",
|
|
9
|
+
]);
|
|
10
|
+
// ── Nuclear entry generation ──────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Generate nuclear entries from a logical turn (a sequence of messages
|
|
13
|
+
* starting with a user message, followed by assistant + tool messages).
|
|
14
|
+
*/
|
|
15
|
+
export function toNuclearEntries(messages, startSeq, instanceId) {
|
|
16
|
+
const entries = [];
|
|
17
|
+
let seq = startSeq;
|
|
18
|
+
const ts = Date.now();
|
|
19
|
+
for (const msg of messages) {
|
|
20
|
+
if (msg.role === "user") {
|
|
21
|
+
const text = typeof msg.content === "string" ? msg.content : "";
|
|
22
|
+
// Skip compaction markers
|
|
23
|
+
if (text.startsWith("["))
|
|
24
|
+
continue;
|
|
25
|
+
entries.push({
|
|
26
|
+
seq: seq++, ts, iid: instanceId,
|
|
27
|
+
kind: "user",
|
|
28
|
+
sum: `user: "${truncate(text, 80)}"`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else if (msg.role === "assistant") {
|
|
32
|
+
// Process tool calls
|
|
33
|
+
if ("tool_calls" in msg && msg.tool_calls) {
|
|
34
|
+
for (const tc of msg.tool_calls) {
|
|
35
|
+
if (!("function" in tc))
|
|
36
|
+
continue;
|
|
37
|
+
const name = tc.function.name;
|
|
38
|
+
let args = {};
|
|
39
|
+
try {
|
|
40
|
+
args = JSON.parse(tc.function.arguments);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
// Store the tool call — we'll enrich it when we see the result
|
|
44
|
+
entries.push({
|
|
45
|
+
seq: seq++, ts, iid: instanceId,
|
|
46
|
+
kind: "tool",
|
|
47
|
+
tool: name,
|
|
48
|
+
sum: summarizeToolCall(name, args),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (typeof msg.content === "string" && msg.content) {
|
|
53
|
+
entries.push({
|
|
54
|
+
seq: seq++, ts, iid: instanceId,
|
|
55
|
+
kind: "agent",
|
|
56
|
+
sum: `agent: "${truncate(msg.content, 60)}"`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (msg.role === "tool") {
|
|
61
|
+
// Enrich the most recent tool entry with result info
|
|
62
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
63
|
+
const lastTool = findLastTool(entries);
|
|
64
|
+
if (lastTool) {
|
|
65
|
+
const isError = content.startsWith("Error:");
|
|
66
|
+
if (isError) {
|
|
67
|
+
lastTool.kind = "error";
|
|
68
|
+
lastTool.sum = `error: ${lastTool.tool} ${truncate(content.slice(7).trim(), 80)}`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
lastTool.sum = enrichWithResult(lastTool.tool ?? "", lastTool.sum, content);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
// ── Formatting ────────────────────────────────────────────────────
|
|
79
|
+
/** Format a nuclear entry as a display line (for in-context injection). */
|
|
80
|
+
export function formatNuclearLine(entry) {
|
|
81
|
+
const time = new Date(entry.ts).toLocaleTimeString("en-US", {
|
|
82
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
83
|
+
});
|
|
84
|
+
return `#${entry.seq} ${time} ${entry.sum}`;
|
|
85
|
+
}
|
|
86
|
+
// ── Serialization (JSONL for history file) ────────────────────────
|
|
87
|
+
/** Serialize a nuclear entry to a JSONL line. */
|
|
88
|
+
export function serializeEntry(entry) {
|
|
89
|
+
return JSON.stringify(entry);
|
|
90
|
+
}
|
|
91
|
+
/** Deserialize a JSONL line to a nuclear entry. Returns null on parse failure. */
|
|
92
|
+
export function deserializeEntry(line) {
|
|
93
|
+
try {
|
|
94
|
+
const obj = JSON.parse(line);
|
|
95
|
+
if (typeof obj.seq === "number" && typeof obj.sum === "string") {
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Classification helpers ────────────────────────────────────────
|
|
105
|
+
/** Check if a nuclear entry represents a read-only action (should be dropped). */
|
|
106
|
+
export function isReadOnly(entry) {
|
|
107
|
+
return entry.kind === "tool" && entry.tool != null && READ_ONLY_TOOLS.has(entry.tool);
|
|
108
|
+
}
|
|
109
|
+
// ── Internal helpers ──────────────────────────────────────────────
|
|
110
|
+
function truncate(text, maxLen) {
|
|
111
|
+
const oneLine = text.replace(/\n/g, " ").trim();
|
|
112
|
+
return oneLine.length > maxLen ? oneLine.slice(0, maxLen) + "..." : oneLine;
|
|
113
|
+
}
|
|
114
|
+
function findLastTool(entries) {
|
|
115
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
116
|
+
if (entries[i].kind === "tool")
|
|
117
|
+
return entries[i];
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function summarizeToolCall(name, args) {
|
|
122
|
+
switch (name) {
|
|
123
|
+
case "bash":
|
|
124
|
+
return `bash: ${truncate(String(args.command ?? ""), 60)}`;
|
|
125
|
+
case "user_shell":
|
|
126
|
+
return `user_shell: ${truncate(String(args.command ?? ""), 60)}`;
|
|
127
|
+
case "edit_file":
|
|
128
|
+
return `edit_file ${args.path ?? ""}`;
|
|
129
|
+
case "write_file":
|
|
130
|
+
case "write":
|
|
131
|
+
return `write_file ${args.path ?? args.file_path ?? ""}`;
|
|
132
|
+
case "read_file":
|
|
133
|
+
return `read_file ${args.path ?? args.file_path ?? ""}`;
|
|
134
|
+
case "grep":
|
|
135
|
+
return `grep "${truncate(String(args.pattern ?? ""), 30)}"`;
|
|
136
|
+
case "glob":
|
|
137
|
+
return `glob ${args.pattern ?? ""}`;
|
|
138
|
+
case "ls":
|
|
139
|
+
return `ls ${args.path ?? "."}`;
|
|
140
|
+
case "display":
|
|
141
|
+
return `display: ${truncate(String(args.command ?? ""), 60)}`;
|
|
142
|
+
default:
|
|
143
|
+
return `${name}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function enrichWithResult(toolName, summary, result) {
|
|
147
|
+
const lines = result.split("\n");
|
|
148
|
+
const lineCount = lines.length;
|
|
149
|
+
switch (toolName) {
|
|
150
|
+
case "bash":
|
|
151
|
+
case "user_shell": {
|
|
152
|
+
// Extract exit code from result if present
|
|
153
|
+
const exitMatch = result.match(/exit code[:\s]*(\d+)/i) ?? result.match(/exit\s+(\d+)/);
|
|
154
|
+
const exitCode = exitMatch ? exitMatch[1] : "0";
|
|
155
|
+
return `${summary} (exit ${exitCode}, ${lineCount} lines)`;
|
|
156
|
+
}
|
|
157
|
+
case "edit_file":
|
|
158
|
+
case "edit": {
|
|
159
|
+
// Try to extract +/- counts from result
|
|
160
|
+
const addMatch = result.match(/\+(\d+)/);
|
|
161
|
+
const delMatch = result.match(/-(\d+)/);
|
|
162
|
+
if (addMatch || delMatch) {
|
|
163
|
+
return `${summary} (+${addMatch?.[1] ?? 0}/-${delMatch?.[1] ?? 0})`;
|
|
164
|
+
}
|
|
165
|
+
return `${summary} (edited)`;
|
|
166
|
+
}
|
|
167
|
+
case "write_file":
|
|
168
|
+
case "write": {
|
|
169
|
+
const created = result.toLowerCase().includes("created") ? "created" : "written";
|
|
170
|
+
return `${summary} (${created}, ${lineCount} lines)`;
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
return `${summary} (${lineCount} lines)`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -4,11 +4,11 @@ import type { ContextManager } from "../context-manager.js";
|
|
|
4
4
|
* Static system prompt — identical across all queries, cacheable.
|
|
5
5
|
* Contains only identity and behavioral instructions.
|
|
6
6
|
*/
|
|
7
|
-
export declare const STATIC_SYSTEM_PROMPT = "You are an AI coding assistant embedded in agent-sh, a terminal shell.\nYou have access to the user's shell environment and can read, write, and execute code.\nYou share the user's working directory, environment variables, and shell history.\n\n# Tool Decision Guide\n\nYou have three categories of tools \u2014 choose based on who needs the output and\nwhether the command has lasting effects:\n\n**Scratchpad tools** (bash, read_file, grep, glob, ls, edit_file, write_file):\nUse these to investigate, search, read, and modify files. Output is returned\nto you for reasoning \u2014 the user doesn't see it directly.\n\n**Display** (display):\nUse this to show output to the user in their terminal. The user sees the\noutput directly, but it is NOT returned to you. Use when:\n- The user asks to see something (cat a file, git log, git diff, man page)\n- The output is for the user to read, not for you to process\n\n**Live shell** (user_shell):\nUse this to run
|
|
7
|
+
export declare const STATIC_SYSTEM_PROMPT = "You are an AI coding assistant embedded in agent-sh, a terminal shell.\nYou have access to the user's shell environment and can read, write, and execute code.\nYou share the user's working directory, environment variables, and shell history.\n\n# Tool Decision Guide\n\nYou have three categories of tools \u2014 choose based on who needs the output and\nwhether the command has lasting effects:\n\n**Scratchpad tools** (bash, read_file, grep, glob, ls, edit_file, write_file):\nUse these to investigate, search, read, and modify files. Output is returned\nto you for reasoning \u2014 the user doesn't see it directly.\n\n**Display** (display):\nUse this to show output to the user in their terminal. The user sees the\noutput directly, but it is NOT returned to you. Use when:\n- The user asks to see something (cat a file, git log, git diff, man page)\n- The output is for the user to read, not for you to process\n\n**Live shell** (user_shell):\nUse this to run complete, non-interactive commands in the user's real shell. Use for:\n- Commands that affect shell state (cd, export, source)\n- Installing packages, starting servers, running builds\n- Any command where the user wants real side effects\n- Set return_output=true only if you need to inspect the result\n\n**Terminal interaction** (terminal_read, terminal_keys):\nUse these to observe and interact with what is currently on the user's terminal screen.\n- terminal_read: see what the user sees (current screen contents, cursor position)\n- terminal_keys: send keystrokes as if the user typed them\nUse for: driving interactive programs (vim, htop, less, ssh, REPLs), answering questions\nabout what's on screen, or typing at the shell prompt when a program is already running.\nDo NOT use user_shell to interact with an already-running program \u2014 use these instead.\n\nDefault to scratchpad tools for your own investigation. Use display when the\nuser is the intended audience. Use user_shell when the command has real effects.\nUse terminal_read/terminal_keys when interacting with what's already on screen.\n\n# Interactive Overlay Sessions\n\nWhen the dynamic context includes `interactive-session: true`, the user has summoned you\nvia a hotkey overlay from inside their live terminal. They may be in the middle of using\na program (vim, ssh, a REPL, etc.) or at a shell prompt. In this mode:\n- Start with terminal_read if you need to understand what's on screen.\n- Prefer terminal_keys to interact with whatever is currently running.\n- Use user_shell only for running new, standalone commands \u2014 not for interacting with\n what's already on screen.\n- Keep responses concise \u2014 the user is in the middle of a workflow.\n\n# Tool Usage Guidelines\n- Use read_file before editing a file you haven't seen\n- Prefer edit_file over write_file for modifying existing files\n- Use grep/glob to find files before reading them\n- Keep bash commands focused; avoid long-running blocking commands\n- Always check command exit codes for errors";
|
|
8
8
|
/**
|
|
9
9
|
* Build the dynamic context — injected as a user message before each query.
|
|
10
10
|
* Contains everything that changes: tools, shell context, conventions, cwd.
|
|
11
11
|
*
|
|
12
12
|
* Runs through the "agent:dynamic-context" pipe so extensions can append.
|
|
13
13
|
*/
|
|
14
|
-
export declare function buildDynamicContext(tools: ToolDefinition[], contextManager: ContextManager): string;
|
|
14
|
+
export declare function buildDynamicContext(tools: ToolDefinition[], contextManager: ContextManager, shellBudgetTokens?: number): string;
|
|
@@ -56,14 +56,34 @@ output directly, but it is NOT returned to you. Use when:
|
|
|
56
56
|
- The output is for the user to read, not for you to process
|
|
57
57
|
|
|
58
58
|
**Live shell** (user_shell):
|
|
59
|
-
Use this to run
|
|
59
|
+
Use this to run complete, non-interactive commands in the user's real shell. Use for:
|
|
60
60
|
- Commands that affect shell state (cd, export, source)
|
|
61
61
|
- Installing packages, starting servers, running builds
|
|
62
62
|
- Any command where the user wants real side effects
|
|
63
63
|
- Set return_output=true only if you need to inspect the result
|
|
64
64
|
|
|
65
|
+
**Terminal interaction** (terminal_read, terminal_keys):
|
|
66
|
+
Use these to observe and interact with what is currently on the user's terminal screen.
|
|
67
|
+
- terminal_read: see what the user sees (current screen contents, cursor position)
|
|
68
|
+
- terminal_keys: send keystrokes as if the user typed them
|
|
69
|
+
Use for: driving interactive programs (vim, htop, less, ssh, REPLs), answering questions
|
|
70
|
+
about what's on screen, or typing at the shell prompt when a program is already running.
|
|
71
|
+
Do NOT use user_shell to interact with an already-running program — use these instead.
|
|
72
|
+
|
|
65
73
|
Default to scratchpad tools for your own investigation. Use display when the
|
|
66
74
|
user is the intended audience. Use user_shell when the command has real effects.
|
|
75
|
+
Use terminal_read/terminal_keys when interacting with what's already on screen.
|
|
76
|
+
|
|
77
|
+
# Interactive Overlay Sessions
|
|
78
|
+
|
|
79
|
+
When the dynamic context includes \`interactive-session: true\`, the user has summoned you
|
|
80
|
+
via a hotkey overlay from inside their live terminal. They may be in the middle of using
|
|
81
|
+
a program (vim, ssh, a REPL, etc.) or at a shell prompt. In this mode:
|
|
82
|
+
- Start with terminal_read if you need to understand what's on screen.
|
|
83
|
+
- Prefer terminal_keys to interact with whatever is currently running.
|
|
84
|
+
- Use user_shell only for running new, standalone commands — not for interacting with
|
|
85
|
+
what's already on screen.
|
|
86
|
+
- Keep responses concise — the user is in the middle of a workflow.
|
|
67
87
|
|
|
68
88
|
# Tool Usage Guidelines
|
|
69
89
|
- Use read_file before editing a file you haven't seen
|
|
@@ -77,7 +97,7 @@ user is the intended audience. Use user_shell when the command has real effects.
|
|
|
77
97
|
*
|
|
78
98
|
* Runs through the "agent:dynamic-context" pipe so extensions can append.
|
|
79
99
|
*/
|
|
80
|
-
export function buildDynamicContext(tools, contextManager) {
|
|
100
|
+
export function buildDynamicContext(tools, contextManager, shellBudgetTokens) {
|
|
81
101
|
const sections = [];
|
|
82
102
|
// Tools
|
|
83
103
|
sections.push("# Available Tools\n" +
|
|
@@ -92,8 +112,9 @@ export function buildDynamicContext(tools, contextManager) {
|
|
|
92
112
|
if (skills.length > 0) {
|
|
93
113
|
sections.push(`You have access to ${skills.length} skill(s). Use the list_skills tool to see them, then read_file to load one.`);
|
|
94
114
|
}
|
|
95
|
-
// Shell context
|
|
96
|
-
const
|
|
115
|
+
// Shell context — pass token budget converted to bytes (~4 chars/token)
|
|
116
|
+
const shellBudgetBytes = shellBudgetTokens != null ? shellBudgetTokens * 4 : undefined;
|
|
117
|
+
const shellContext = contextManager.getContext(shellBudgetBytes);
|
|
97
118
|
if (shellContext) {
|
|
98
119
|
sections.push(shellContext);
|
|
99
120
|
}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
export function createUserShellTool(opts) {
|
|
9
9
|
return {
|
|
10
10
|
name: "user_shell",
|
|
11
|
-
description: "Run a
|
|
11
|
+
description: "Run a complete, non-interactive command in the user's live shell (cd, export, install packages, start servers, git commands). " +
|
|
12
|
+
"Use this for commands that have side effects or that the user wants to see. Output is shown directly to the user but NOT returned " +
|
|
13
|
+
"to you by default — set return_output=true if you need to inspect the result. " +
|
|
14
|
+
"Do NOT use this to interact with programs that are already running in the terminal — use terminal_keys/terminal_read instead.",
|
|
12
15
|
input_schema: {
|
|
13
16
|
type: "object",
|
|
14
17
|
properties: {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { EventBus } from "./event-bus.js";
|
|
2
|
+
import type { HandlerRegistry } from "./utils/handler-registry.js";
|
|
2
3
|
export declare class ContextManager {
|
|
3
4
|
private exchanges;
|
|
4
5
|
private nextId;
|
|
5
6
|
private currentCwd;
|
|
6
7
|
private sessionStart;
|
|
7
|
-
private pendingToolCalls;
|
|
8
8
|
private firstPrompt;
|
|
9
9
|
private agentShellActive;
|
|
10
|
-
|
|
10
|
+
private handlers;
|
|
11
|
+
constructor(bus: EventBus, handlers?: HandlerRegistry);
|
|
11
12
|
getCwd(): string;
|
|
12
13
|
/**
|
|
13
14
|
* Build the <shell_context> block for the agent prompt.
|
package/dist/context-manager.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { getSettings } from "./settings.js";
|
|
2
|
-
// Non-configurable thresholds (agent response and tool output follow shell settings)
|
|
3
|
-
const AGENT_RESPONSE_TRUNCATE_THRESHOLD = 20;
|
|
4
|
-
const AGENT_RESPONSE_HEAD_LINES = 15;
|
|
5
|
-
const TOOL_TRUNCATE_THRESHOLD = 20;
|
|
6
|
-
const TOOL_HEAD_LINES = 5;
|
|
7
|
-
const TOOL_TAIL_LINES = 5;
|
|
8
2
|
export class ContextManager {
|
|
9
3
|
exchanges = [];
|
|
10
4
|
nextId = 1;
|
|
11
5
|
currentCwd;
|
|
12
6
|
sessionStart;
|
|
13
|
-
pendingToolCalls = [];
|
|
14
7
|
firstPrompt = true;
|
|
15
8
|
agentShellActive = false; // true while user_shell command is executing
|
|
16
|
-
|
|
9
|
+
handlers = null;
|
|
10
|
+
constructor(bus, handlers) {
|
|
11
|
+
if (handlers) {
|
|
12
|
+
this.handlers = handlers;
|
|
13
|
+
// Extensions can advise this to inject extra context (e.g. terminal buffer)
|
|
14
|
+
handlers.define("context:build-extra", () => "");
|
|
15
|
+
}
|
|
17
16
|
this.currentCwd = process.cwd();
|
|
18
17
|
this.sessionStart = Date.now();
|
|
19
18
|
// ── Subscribe to shell events ──
|
|
@@ -37,46 +36,11 @@ export class ContextManager {
|
|
|
37
36
|
bus.on("shell:agent-exec-start", () => { this.agentShellActive = true; });
|
|
38
37
|
bus.on("shell:agent-exec-done", () => { this.agentShellActive = false; });
|
|
39
38
|
// ── Subscribe to agent events ──
|
|
39
|
+
// Only track queries (as markers). Agent responses and tool outputs
|
|
40
|
+
// live exclusively in ConversationState to avoid duplication.
|
|
40
41
|
bus.on("agent:query", (e) => {
|
|
41
|
-
this.pendingToolCalls = [];
|
|
42
42
|
this.addExchange({ type: "agent_query", query: e.query });
|
|
43
43
|
});
|
|
44
|
-
bus.on("agent:response-done", (e) => {
|
|
45
|
-
this.addExchange({
|
|
46
|
-
type: "agent_response",
|
|
47
|
-
response: e.response,
|
|
48
|
-
toolCalls: this.pendingToolCalls,
|
|
49
|
-
});
|
|
50
|
-
this.pendingToolCalls = [];
|
|
51
|
-
});
|
|
52
|
-
bus.on("agent:tool-call", (e) => {
|
|
53
|
-
// Accumulate tool calls for the agent_response summary
|
|
54
|
-
this.pendingToolCalls.push({
|
|
55
|
-
tool: e.tool,
|
|
56
|
-
args: e.args,
|
|
57
|
-
output: "",
|
|
58
|
-
exitCode: null,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
bus.on("agent:tool-output", (e) => {
|
|
62
|
-
// Update the last pending tool call with output
|
|
63
|
-
const last = this.pendingToolCalls[this.pendingToolCalls.length - 1];
|
|
64
|
-
if (last) {
|
|
65
|
-
last.output = e.output;
|
|
66
|
-
last.exitCode = e.exitCode;
|
|
67
|
-
}
|
|
68
|
-
// Also store as a separate exchange for chronological log
|
|
69
|
-
const lines = e.output.split("\n");
|
|
70
|
-
this.addExchange({
|
|
71
|
-
type: "tool_execution",
|
|
72
|
-
tool: e.tool,
|
|
73
|
-
args: {},
|
|
74
|
-
output: e.output,
|
|
75
|
-
exitCode: e.exitCode,
|
|
76
|
-
outputLines: lines.length,
|
|
77
|
-
outputBytes: e.output.length,
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
44
|
}
|
|
81
45
|
// ── Public query API ──────────────────────────────────────────
|
|
82
46
|
getCwd() {
|
|
@@ -225,7 +189,6 @@ export class ContextManager {
|
|
|
225
189
|
*/
|
|
226
190
|
clear() {
|
|
227
191
|
this.exchanges = [];
|
|
228
|
-
this.pendingToolCalls = [];
|
|
229
192
|
this.firstPrompt = true;
|
|
230
193
|
// Don't reset nextId — IDs should be globally unique within a session
|
|
231
194
|
}
|
|
@@ -243,12 +206,7 @@ export class ContextManager {
|
|
|
243
206
|
const s = getSettings();
|
|
244
207
|
ex.output = truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id);
|
|
245
208
|
}
|
|
246
|
-
|
|
247
|
-
ex.response = truncateHead(ex.response, AGENT_RESPONSE_TRUNCATE_THRESHOLD, AGENT_RESPONSE_HEAD_LINES, ex.id);
|
|
248
|
-
}
|
|
249
|
-
else if (ex.type === "tool_execution") {
|
|
250
|
-
ex.output = truncateOutput(ex.output, TOOL_TRUNCATE_THRESHOLD, TOOL_HEAD_LINES, TOOL_TAIL_LINES, ex.id);
|
|
251
|
-
}
|
|
209
|
+
// agent_query has no output to truncate
|
|
252
210
|
}
|
|
253
211
|
// Pass 2: budget enforcement — strip output from oldest if over budget
|
|
254
212
|
let totalSize = result.reduce((sum, ex) => sum + this.exchangeSize(ex), 0);
|
|
@@ -258,12 +216,6 @@ export class ContextManager {
|
|
|
258
216
|
if (ex.type === "shell_command") {
|
|
259
217
|
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
260
218
|
}
|
|
261
|
-
else if (ex.type === "tool_execution") {
|
|
262
|
-
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
263
|
-
}
|
|
264
|
-
else if (ex.type === "agent_response") {
|
|
265
|
-
ex.response = `[response omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
266
|
-
}
|
|
267
219
|
totalSize -= before - this.exchangeSize(ex);
|
|
268
220
|
}
|
|
269
221
|
return result;
|
|
@@ -282,7 +234,8 @@ export class ContextManager {
|
|
|
282
234
|
out += `- When the user asks to see, list, view, or display anything, ALWAYS use user_shell. NEVER use internal tools like ls/read/bash for display — the user won't see it.\n`;
|
|
283
235
|
out += `- Only use internal tools when YOU need to reason about content silently (e.g. reading a file to answer a question about it).\n`;
|
|
284
236
|
out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
|
|
285
|
-
out += `- You can browse or search
|
|
237
|
+
out += `- You can browse or search shell history with shell_recall.\n`;
|
|
238
|
+
out += `- You can browse or search evicted conversation turns with conversation_recall.\n`;
|
|
286
239
|
out += `\n`;
|
|
287
240
|
this.firstPrompt = false;
|
|
288
241
|
}
|
|
@@ -291,6 +244,10 @@ export class ContextManager {
|
|
|
291
244
|
for (const ex of exchanges) {
|
|
292
245
|
out += "\n" + this.formatExchangeTruncated(ex);
|
|
293
246
|
}
|
|
247
|
+
// Allow extensions to inject extra context (e.g. terminal buffer snapshot)
|
|
248
|
+
const extra = this.handlers?.call("context:build-extra");
|
|
249
|
+
if (extra)
|
|
250
|
+
out += "\n" + extra + "\n";
|
|
294
251
|
out += "\n</shell_context>\n";
|
|
295
252
|
return out;
|
|
296
253
|
}
|
|
@@ -316,25 +273,6 @@ export class ContextManager {
|
|
|
316
273
|
}
|
|
317
274
|
case "agent_query":
|
|
318
275
|
return `#${ex.id} [you] > ${ex.query}\n`;
|
|
319
|
-
case "agent_response": {
|
|
320
|
-
let s = `#${ex.id} [agent] `;
|
|
321
|
-
if (ex.response)
|
|
322
|
-
s += ex.response.split("\n")[0] + "\n";
|
|
323
|
-
if (ex.response.includes("\n")) {
|
|
324
|
-
const rest = ex.response.slice(ex.response.indexOf("\n") + 1);
|
|
325
|
-
if (rest.trim())
|
|
326
|
-
s += indent(rest, " ") + "\n";
|
|
327
|
-
}
|
|
328
|
-
return s;
|
|
329
|
-
}
|
|
330
|
-
case "tool_execution": {
|
|
331
|
-
let s = `#${ex.id} [tool] ${ex.tool}\n`;
|
|
332
|
-
if (ex.output)
|
|
333
|
-
s += indent(ex.output, " ") + "\n";
|
|
334
|
-
if (ex.exitCode !== null)
|
|
335
|
-
s += ` exit ${ex.exitCode}\n`;
|
|
336
|
-
return s;
|
|
337
|
-
}
|
|
338
276
|
}
|
|
339
277
|
}
|
|
340
278
|
formatExchangeFull(ex) {
|
|
@@ -351,16 +289,6 @@ export class ContextManager {
|
|
|
351
289
|
}
|
|
352
290
|
case "agent_query":
|
|
353
291
|
return `#${ex.id} [you] > ${ex.query}`;
|
|
354
|
-
case "agent_response":
|
|
355
|
-
return `#${ex.id} [agent]\n${ex.response}`;
|
|
356
|
-
case "tool_execution": {
|
|
357
|
-
let s = `#${ex.id} [tool] ${ex.tool} (${ex.outputLines} lines, ${ex.outputBytes} bytes)\n`;
|
|
358
|
-
if (ex.output)
|
|
359
|
-
s += ex.output + "\n";
|
|
360
|
-
if (ex.exitCode !== null)
|
|
361
|
-
s += `exit ${ex.exitCode}\n`;
|
|
362
|
-
return s;
|
|
363
|
-
}
|
|
364
292
|
}
|
|
365
293
|
}
|
|
366
294
|
exchangeOneLiner(ex) {
|
|
@@ -371,12 +299,6 @@ export class ContextManager {
|
|
|
371
299
|
}
|
|
372
300
|
case "agent_query":
|
|
373
301
|
return `#${ex.id} query: ${ex.query}`;
|
|
374
|
-
case "agent_response": {
|
|
375
|
-
const preview = ex.response.split("\n")[0]?.slice(0, 80) ?? "";
|
|
376
|
-
return `#${ex.id} agent: ${preview}${ex.response.length > 80 ? "..." : ""}`;
|
|
377
|
-
}
|
|
378
|
-
case "tool_execution":
|
|
379
|
-
return `#${ex.id} tool: ${ex.tool} (${ex.outputLines} lines, exit ${ex.exitCode ?? "?"})`;
|
|
380
302
|
}
|
|
381
303
|
}
|
|
382
304
|
exchangeSearchText(ex) {
|
|
@@ -385,10 +307,6 @@ export class ContextManager {
|
|
|
385
307
|
return `${ex.command}\n${ex.output}`;
|
|
386
308
|
case "agent_query":
|
|
387
309
|
return ex.query;
|
|
388
|
-
case "agent_response":
|
|
389
|
-
return ex.response;
|
|
390
|
-
case "tool_execution":
|
|
391
|
-
return `${ex.tool}\n${ex.output}`;
|
|
392
310
|
}
|
|
393
311
|
}
|
|
394
312
|
exchangeSize(ex) {
|
|
@@ -397,10 +315,6 @@ export class ContextManager {
|
|
|
397
315
|
return ex.command.length + ex.output.length;
|
|
398
316
|
case "agent_query":
|
|
399
317
|
return ex.query.length;
|
|
400
|
-
case "agent_response":
|
|
401
|
-
return ex.response.length;
|
|
402
|
-
case "tool_execution":
|
|
403
|
-
return ex.tool.length + ex.output.length;
|
|
404
318
|
}
|
|
405
319
|
}
|
|
406
320
|
}
|
|
@@ -416,15 +330,6 @@ function truncateOutput(text, threshold, headLines, tailLines, id) {
|
|
|
416
330
|
...lines.slice(-tailLines),
|
|
417
331
|
].join("\n");
|
|
418
332
|
}
|
|
419
|
-
function truncateHead(text, threshold, headLines, id) {
|
|
420
|
-
const lines = text.split("\n");
|
|
421
|
-
if (lines.length <= threshold)
|
|
422
|
-
return text;
|
|
423
|
-
return [
|
|
424
|
-
...lines.slice(0, headLines),
|
|
425
|
-
`[... truncated, use shell_recall tool with expand and id ${id} for full response ...]`,
|
|
426
|
-
].join("\n");
|
|
427
|
-
}
|
|
428
333
|
function indent(text, prefix) {
|
|
429
334
|
return text
|
|
430
335
|
.split("\n")
|
package/dist/core.js
CHANGED
|
@@ -25,6 +25,8 @@ import * as streamTransform from "./utils/stream-transform.js";
|
|
|
25
25
|
import * as settingsMod from "./settings.js";
|
|
26
26
|
import { resolveProvider, getProviderNames } from "./settings.js";
|
|
27
27
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
28
|
+
import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
29
|
+
import { FloatingPanel } from "./utils/floating-panel.js";
|
|
28
30
|
// Re-export types that library consumers need
|
|
29
31
|
export { EventBus } from "./event-bus.js";
|
|
30
32
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -33,7 +35,7 @@ export { LlmClient } from "./utils/llm-client.js";
|
|
|
33
35
|
export function createCore(config) {
|
|
34
36
|
const bus = new EventBus();
|
|
35
37
|
const handlers = new HandlerRegistry();
|
|
36
|
-
const contextManager = new ContextManager(bus);
|
|
38
|
+
const contextManager = new ContextManager(bus, handlers);
|
|
37
39
|
// ── Resolve provider ─────────────────────────────────────────
|
|
38
40
|
const settings = settingsMod.getSettings();
|
|
39
41
|
let activeProvider = null;
|
|
@@ -173,6 +175,20 @@ export function createCore(config) {
|
|
|
173
175
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
174
176
|
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
175
177
|
});
|
|
178
|
+
// Push registered models into the agent loop so they appear in
|
|
179
|
+
// autocomplete and are selectable via /model.
|
|
180
|
+
const addModes = modelIds.map((m) => {
|
|
181
|
+
const mc = caps.get(m);
|
|
182
|
+
return {
|
|
183
|
+
model: m,
|
|
184
|
+
provider: p.id,
|
|
185
|
+
providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
|
|
186
|
+
contextWindow: mc?.contextWindow,
|
|
187
|
+
reasoning: mc?.reasoning,
|
|
188
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
bus.emit("config:add-modes", { modes: addModes });
|
|
176
192
|
});
|
|
177
193
|
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
178
194
|
const p = providerRegistry.get(name);
|
|
@@ -216,6 +232,14 @@ export function createCore(config) {
|
|
|
216
232
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
217
233
|
bus.emit("config:changed", {});
|
|
218
234
|
});
|
|
235
|
+
// ── Lazy singleton terminal buffer ──────────────────────────
|
|
236
|
+
let terminalBufferSingleton; // undefined = not yet created
|
|
237
|
+
const getTerminalBuffer = () => {
|
|
238
|
+
if (terminalBufferSingleton !== undefined)
|
|
239
|
+
return terminalBufferSingleton;
|
|
240
|
+
terminalBufferSingleton = TerminalBuffer.createWired(bus);
|
|
241
|
+
return terminalBufferSingleton;
|
|
242
|
+
};
|
|
219
243
|
return {
|
|
220
244
|
bus,
|
|
221
245
|
contextManager,
|
|
@@ -292,6 +316,11 @@ export function createCore(config) {
|
|
|
292
316
|
define: (name, fn) => handlers.define(name, fn),
|
|
293
317
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
294
318
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
319
|
+
get terminalBuffer() { return getTerminalBuffer(); },
|
|
320
|
+
createFloatingPanel: (config) => {
|
|
321
|
+
const tb = config.dimBackground !== false ? getTerminalBuffer() : null;
|
|
322
|
+
return new FloatingPanel(bus, { ...config, terminalBuffer: tb ?? undefined });
|
|
323
|
+
},
|
|
295
324
|
};
|
|
296
325
|
},
|
|
297
326
|
kill() {
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -22,6 +22,25 @@ export interface ShellEvents {
|
|
|
22
22
|
};
|
|
23
23
|
"shell:agent-exec-start": Record<string, never>;
|
|
24
24
|
"shell:agent-exec-done": Record<string, never>;
|
|
25
|
+
"shell:pty-data": {
|
|
26
|
+
raw: string;
|
|
27
|
+
};
|
|
28
|
+
"shell:pty-write": {
|
|
29
|
+
data: string;
|
|
30
|
+
};
|
|
31
|
+
"shell:pty-resize": {
|
|
32
|
+
cols: number;
|
|
33
|
+
rows: number;
|
|
34
|
+
};
|
|
35
|
+
"shell:buffer-request": Record<string, never>;
|
|
36
|
+
"shell:buffer-snapshot": {
|
|
37
|
+
text: string;
|
|
38
|
+
altScreen: boolean;
|
|
39
|
+
cursor: {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
25
44
|
"agent:submit": {
|
|
26
45
|
query: string;
|
|
27
46
|
};
|
|
@@ -123,6 +142,14 @@ export interface ShellEvents {
|
|
|
123
142
|
"input:keypress": {
|
|
124
143
|
key: string;
|
|
125
144
|
};
|
|
145
|
+
"input:intercept": {
|
|
146
|
+
data: string;
|
|
147
|
+
consumed: boolean;
|
|
148
|
+
};
|
|
149
|
+
"shell:stdout-hold": Record<string, never>;
|
|
150
|
+
"shell:stdout-release": Record<string, never>;
|
|
151
|
+
"shell:stdout-show": Record<string, never>;
|
|
152
|
+
"shell:stdout-hide": Record<string, never>;
|
|
126
153
|
"agent:terminal-intercept": {
|
|
127
154
|
command: string;
|
|
128
155
|
cwd: string;
|
|
@@ -148,6 +175,13 @@ export interface ShellEvents {
|
|
|
148
175
|
contextWindow?: number;
|
|
149
176
|
};
|
|
150
177
|
"agent:reset-session": Record<string, never>;
|
|
178
|
+
"agent:compact-request": Record<string, never>;
|
|
179
|
+
"context:get-stats": {
|
|
180
|
+
activeTokens: number;
|
|
181
|
+
nuclearEntries: number;
|
|
182
|
+
recallArchiveSize: number;
|
|
183
|
+
budgetTokens: number;
|
|
184
|
+
};
|
|
151
185
|
"agent:register-backend": {
|
|
152
186
|
name: string;
|
|
153
187
|
kill: () => void;
|
|
@@ -187,6 +221,9 @@ export interface ShellEvents {
|
|
|
187
221
|
"config:set-modes": {
|
|
188
222
|
modes: AgentMode[];
|
|
189
223
|
};
|
|
224
|
+
"config:add-modes": {
|
|
225
|
+
modes: AgentMode[];
|
|
226
|
+
};
|
|
190
227
|
"provider:register": {
|
|
191
228
|
id: string;
|
|
192
229
|
apiKey?: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in overlay agent.
|
|
3
|
+
*
|
|
4
|
+
* Provides a hotkey (Ctrl+\) to summon the agent from anywhere — even
|
|
5
|
+
* inside vim, htop, or ssh. Composites a floating response box on top
|
|
6
|
+
* of the current terminal content.
|
|
7
|
+
*
|
|
8
|
+
* Rendering reuses the shared tui:render-* handlers so that extensions
|
|
9
|
+
* advising those handlers affect both the main TUI and the overlay.
|
|
10
|
+
*
|
|
11
|
+
* Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionContext } from "../types.js";
|
|
14
|
+
export default function activate(ctx: ExtensionContext): void;
|