agent-sh 0.7.0 → 0.9.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 +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -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 +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -11
- package/dist/extensions/overlay-agent.js +0 -43
- package/examples/extensions/terminal-buffer.ts +0 -184
|
@@ -0,0 +1,176 @@
|
|
|
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 d = new Date(entry.ts);
|
|
82
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
83
|
+
// ISO-ish compact: 2026-04-13 14:05
|
|
84
|
+
const stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
85
|
+
return `#${entry.seq} [${stamp}] ${entry.sum}`;
|
|
86
|
+
}
|
|
87
|
+
// ── Serialization (JSONL for history file) ────────────────────────
|
|
88
|
+
/** Serialize a nuclear entry to a JSONL line. */
|
|
89
|
+
export function serializeEntry(entry) {
|
|
90
|
+
return JSON.stringify(entry);
|
|
91
|
+
}
|
|
92
|
+
/** Deserialize a JSONL line to a nuclear entry. Returns null on parse failure. */
|
|
93
|
+
export function deserializeEntry(line) {
|
|
94
|
+
try {
|
|
95
|
+
const obj = JSON.parse(line);
|
|
96
|
+
if (typeof obj.seq === "number" && typeof obj.sum === "string") {
|
|
97
|
+
return obj;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ── Classification helpers ────────────────────────────────────────
|
|
106
|
+
/** Check if a nuclear entry represents a read-only action (should be dropped). */
|
|
107
|
+
export function isReadOnly(entry) {
|
|
108
|
+
return entry.kind === "tool" && entry.tool != null && READ_ONLY_TOOLS.has(entry.tool);
|
|
109
|
+
}
|
|
110
|
+
// ── Internal helpers ──────────────────────────────────────────────
|
|
111
|
+
function truncate(text, maxLen) {
|
|
112
|
+
const oneLine = text.replace(/\n/g, " ").trim();
|
|
113
|
+
return oneLine.length > maxLen ? oneLine.slice(0, maxLen) + "..." : oneLine;
|
|
114
|
+
}
|
|
115
|
+
function findLastTool(entries) {
|
|
116
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
117
|
+
if (entries[i].kind === "tool")
|
|
118
|
+
return entries[i];
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
function summarizeToolCall(name, args) {
|
|
123
|
+
switch (name) {
|
|
124
|
+
case "bash":
|
|
125
|
+
return `bash: ${truncate(String(args.command ?? ""), 60)}`;
|
|
126
|
+
case "user_shell":
|
|
127
|
+
return `user_shell: ${truncate(String(args.command ?? ""), 60)}`;
|
|
128
|
+
case "edit_file":
|
|
129
|
+
return `edit_file ${args.path ?? ""}`;
|
|
130
|
+
case "write_file":
|
|
131
|
+
case "write":
|
|
132
|
+
return `write_file ${args.path ?? args.file_path ?? ""}`;
|
|
133
|
+
case "read_file":
|
|
134
|
+
return `read_file ${args.path ?? args.file_path ?? ""}`;
|
|
135
|
+
case "grep":
|
|
136
|
+
return `grep "${truncate(String(args.pattern ?? ""), 30)}"`;
|
|
137
|
+
case "glob":
|
|
138
|
+
return `glob ${args.pattern ?? ""}`;
|
|
139
|
+
case "ls":
|
|
140
|
+
return `ls ${args.path ?? "."}`;
|
|
141
|
+
case "display":
|
|
142
|
+
return `display: ${truncate(String(args.command ?? ""), 60)}`;
|
|
143
|
+
default:
|
|
144
|
+
return `${name}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function enrichWithResult(toolName, summary, result) {
|
|
148
|
+
const lines = result.split("\n");
|
|
149
|
+
const lineCount = lines.length;
|
|
150
|
+
switch (toolName) {
|
|
151
|
+
case "bash":
|
|
152
|
+
case "user_shell": {
|
|
153
|
+
// Extract exit code from result if present
|
|
154
|
+
const exitMatch = result.match(/exit code[:\s]*(\d+)/i) ?? result.match(/exit\s+(\d+)/);
|
|
155
|
+
const exitCode = exitMatch ? exitMatch[1] : "0";
|
|
156
|
+
return `${summary} (exit ${exitCode}, ${lineCount} lines)`;
|
|
157
|
+
}
|
|
158
|
+
case "edit_file":
|
|
159
|
+
case "edit": {
|
|
160
|
+
// Try to extract +/- counts from result
|
|
161
|
+
const addMatch = result.match(/\+(\d+)/);
|
|
162
|
+
const delMatch = result.match(/-(\d+)/);
|
|
163
|
+
if (addMatch || delMatch) {
|
|
164
|
+
return `${summary} (+${addMatch?.[1] ?? 0}/-${delMatch?.[1] ?? 0})`;
|
|
165
|
+
}
|
|
166
|
+
return `${summary} (edited)`;
|
|
167
|
+
}
|
|
168
|
+
case "write_file":
|
|
169
|
+
case "write": {
|
|
170
|
+
const created = result.toLowerCase().includes("created") ? "created" : "written";
|
|
171
|
+
return `${summary} (${created}, ${lineCount} lines)`;
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
return `${summary} (${lineCount} lines)`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { ToolDefinition } from "./types.js";
|
|
2
1
|
import type { ContextManager } from "../context-manager.js";
|
|
3
2
|
/**
|
|
4
3
|
* Static system prompt — identical across all queries, cacheable.
|
|
5
4
|
* Contains only identity and behavioral instructions.
|
|
6
5
|
*/
|
|
7
|
-
export declare const STATIC_SYSTEM_PROMPT
|
|
6
|
+
export declare const STATIC_SYSTEM_PROMPT: string;
|
|
8
7
|
/**
|
|
9
8
|
* Build the dynamic context — injected as a user message before each query.
|
|
10
|
-
* Contains everything that changes:
|
|
9
|
+
* Contains everything that changes: shell context, conventions, cwd.
|
|
11
10
|
*
|
|
12
|
-
* Runs through the "
|
|
11
|
+
* Runs through the "dynamic-context:build" handler so extensions can advise.
|
|
13
12
|
*/
|
|
14
|
-
export declare function buildDynamicContext(
|
|
13
|
+
export declare function buildDynamicContext(contextManager: ContextManager, shellBudgetTokens?: number): string;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { discoverSkills } from "./skills.js";
|
|
5
|
+
/** Resolve the absolute path to agent-sh's own docs directory. */
|
|
6
|
+
const DOCS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs");
|
|
4
7
|
/** File names to scan for project conventions (checked in order). */
|
|
5
8
|
const CONVENTION_FILES = ["CLAUDE.md", "AGENT.md"];
|
|
6
9
|
/**
|
|
@@ -36,7 +39,7 @@ function loadConventionFiles(dir) {
|
|
|
36
39
|
* Static system prompt — identical across all queries, cacheable.
|
|
37
40
|
* Contains only identity and behavioral instructions.
|
|
38
41
|
*/
|
|
39
|
-
export const STATIC_SYSTEM_PROMPT = `You are an AI coding assistant embedded in agent-sh, a terminal shell.
|
|
42
|
+
export const STATIC_SYSTEM_PROMPT = `You are ash, an AI coding assistant embedded in agent-sh, a terminal shell.
|
|
40
43
|
You have access to the user's shell environment and can read, write, and execute code.
|
|
41
44
|
You share the user's working directory, environment variables, and shell history.
|
|
42
45
|
|
|
@@ -56,7 +59,7 @@ output directly, but it is NOT returned to you. Use when:
|
|
|
56
59
|
- The output is for the user to read, not for you to process
|
|
57
60
|
|
|
58
61
|
**Live shell** (user_shell):
|
|
59
|
-
Use this to run
|
|
62
|
+
Use this to run complete, non-interactive commands in the user's real shell. Use for:
|
|
60
63
|
- Commands that affect shell state (cd, export, source)
|
|
61
64
|
- Installing packages, starting servers, running builds
|
|
62
65
|
- Any command where the user wants real side effects
|
|
@@ -70,18 +73,19 @@ user is the intended audience. Use user_shell when the command has real effects.
|
|
|
70
73
|
- Prefer edit_file over write_file for modifying existing files
|
|
71
74
|
- Use grep/glob to find files before reading them
|
|
72
75
|
- Keep bash commands focused; avoid long-running blocking commands
|
|
73
|
-
- Always check command exit codes for errors
|
|
76
|
+
- Always check command exit codes for errors
|
|
77
|
+
|
|
78
|
+
# Documentation
|
|
79
|
+
agent-sh documentation is available in: ${DOCS_DIR}
|
|
80
|
+
Use read_file on ${DOCS_DIR}/README.md for an index of all docs.`;
|
|
74
81
|
/**
|
|
75
82
|
* Build the dynamic context — injected as a user message before each query.
|
|
76
|
-
* Contains everything that changes:
|
|
83
|
+
* Contains everything that changes: shell context, conventions, cwd.
|
|
77
84
|
*
|
|
78
|
-
* Runs through the "
|
|
85
|
+
* Runs through the "dynamic-context:build" handler so extensions can advise.
|
|
79
86
|
*/
|
|
80
|
-
export function buildDynamicContext(
|
|
87
|
+
export function buildDynamicContext(contextManager, shellBudgetTokens) {
|
|
81
88
|
const sections = [];
|
|
82
|
-
// Tools
|
|
83
|
-
sections.push("# Available Tools\n" +
|
|
84
|
-
tools.map((t) => `- ${t.name}: ${t.description}`).join("\n"));
|
|
85
89
|
// Project conventions (CLAUDE.md / AGENT.md)
|
|
86
90
|
const conventions = loadConventionFiles(contextManager.getCwd());
|
|
87
91
|
if (conventions.length > 0) {
|
|
@@ -92,8 +96,9 @@ export function buildDynamicContext(tools, contextManager) {
|
|
|
92
96
|
if (skills.length > 0) {
|
|
93
97
|
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
98
|
}
|
|
95
|
-
// Shell context
|
|
96
|
-
const
|
|
99
|
+
// Shell context — pass token budget converted to bytes (~4 chars/token)
|
|
100
|
+
const shellBudgetBytes = shellBudgetTokens != null ? shellBudgetTokens * 4 : undefined;
|
|
101
|
+
const shellContext = contextManager.getContext(shellBudgetBytes);
|
|
97
102
|
if (shellContext) {
|
|
98
103
|
sections.push(shellContext);
|
|
99
104
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class TokenBudget {
|
|
2
|
+
private contextWindow;
|
|
3
|
+
private toolCount;
|
|
4
|
+
constructor(contextWindow?: number, toolCount?: number);
|
|
5
|
+
/** Update when model or tool set changes. */
|
|
6
|
+
update(contextWindow?: number, toolCount?: number): void;
|
|
7
|
+
/** Total tokens available for shell context + conversation content. */
|
|
8
|
+
get contentBudget(): number;
|
|
9
|
+
/** Token budget for the shell context stream. */
|
|
10
|
+
get shellBudgetTokens(): number;
|
|
11
|
+
/** Token budget for the conversation messages stream. */
|
|
12
|
+
get conversationBudgetTokens(): number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified token budget manager.
|
|
3
|
+
*
|
|
4
|
+
* Splits a model's context window between two streams:
|
|
5
|
+
* - Shell context (user shell commands and outputs — situational awareness)
|
|
6
|
+
* - Conversation (agent messages and tool results — task continuity)
|
|
7
|
+
*
|
|
8
|
+
* The budget accounts for fixed overhead (system prompt, tool definitions,
|
|
9
|
+
* response reserve) and divides the remaining space by a configurable ratio.
|
|
10
|
+
*/
|
|
11
|
+
import { getSettings } from "../settings.js";
|
|
12
|
+
/** Overhead estimates (tokens). */
|
|
13
|
+
const SYSTEM_PROMPT_OVERHEAD = 800;
|
|
14
|
+
const DYNAMIC_CONTEXT_OVERHEAD = 500; // conventions, metadata, skills list
|
|
15
|
+
const TOKENS_PER_TOOL_DEFINITION = 50;
|
|
16
|
+
const RESPONSE_RESERVE = 8192; // matches llm-client.ts default max_tokens
|
|
17
|
+
/** Fallback when contextWindow is unknown. */
|
|
18
|
+
const DEFAULT_CONTEXT_WINDOW = 60_000;
|
|
19
|
+
export class TokenBudget {
|
|
20
|
+
contextWindow;
|
|
21
|
+
toolCount;
|
|
22
|
+
constructor(contextWindow, toolCount = 0) {
|
|
23
|
+
this.contextWindow = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
24
|
+
this.toolCount = toolCount;
|
|
25
|
+
}
|
|
26
|
+
/** Update when model or tool set changes. */
|
|
27
|
+
update(contextWindow, toolCount) {
|
|
28
|
+
if (contextWindow != null)
|
|
29
|
+
this.contextWindow = contextWindow;
|
|
30
|
+
if (toolCount != null)
|
|
31
|
+
this.toolCount = toolCount;
|
|
32
|
+
}
|
|
33
|
+
/** Total tokens available for shell context + conversation content. */
|
|
34
|
+
get contentBudget() {
|
|
35
|
+
const overhead = SYSTEM_PROMPT_OVERHEAD +
|
|
36
|
+
DYNAMIC_CONTEXT_OVERHEAD +
|
|
37
|
+
this.toolCount * TOKENS_PER_TOOL_DEFINITION +
|
|
38
|
+
RESPONSE_RESERVE;
|
|
39
|
+
return Math.max(0, this.contextWindow - overhead);
|
|
40
|
+
}
|
|
41
|
+
/** Token budget for the shell context stream. */
|
|
42
|
+
get shellBudgetTokens() {
|
|
43
|
+
const ratio = getSettings().shellContextRatio;
|
|
44
|
+
return Math.floor(this.contentBudget * ratio);
|
|
45
|
+
}
|
|
46
|
+
/** Token budget for the conversation messages stream. */
|
|
47
|
+
get conversationBudgetTokens() {
|
|
48
|
+
return this.contentBudget - this.shellBudgetTokens;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolProtocol — abstracts how tools are presented to the LLM and how
|
|
3
|
+
* tool calls are parsed from responses.
|
|
4
|
+
*
|
|
5
|
+
* Two modes:
|
|
6
|
+
* "api" — tools sent via OpenAI tools param, parsed from delta.tool_calls
|
|
7
|
+
* "inline" — tools described as text, tool calls are JSON code blocks
|
|
8
|
+
*
|
|
9
|
+
* The agent loop uses this interface uniformly so the rest of the code
|
|
10
|
+
* doesn't need to know which mode is active.
|
|
11
|
+
*/
|
|
12
|
+
import type { ChatCompletionTool } from "../utils/llm-client.js";
|
|
13
|
+
import type { ToolDefinition } from "./types.js";
|
|
14
|
+
import type { ConversationState } from "./conversation-state.js";
|
|
15
|
+
export interface PendingToolCall {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
argumentsJson: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ToolResult {
|
|
21
|
+
callId: string;
|
|
22
|
+
toolName: string;
|
|
23
|
+
content: string;
|
|
24
|
+
isError: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Streaming filter — strips tool calls from display output. */
|
|
27
|
+
export interface StreamFilter {
|
|
28
|
+
feed(chunk: string): string;
|
|
29
|
+
flush(): string;
|
|
30
|
+
}
|
|
31
|
+
export interface ToolProtocol {
|
|
32
|
+
readonly mode: string;
|
|
33
|
+
/** Tools to pass in the API request's `tools` parameter. undefined = omit. */
|
|
34
|
+
getApiTools(tools: ToolDefinition[]): ChatCompletionTool[] | undefined;
|
|
35
|
+
/** Extra text for dynamic context (tool catalog for inline mode). */
|
|
36
|
+
getToolPrompt(tools: ToolDefinition[]): string;
|
|
37
|
+
/** Extract tool calls from a completed response. */
|
|
38
|
+
extractToolCalls(responseText: string, streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
39
|
+
/** Rewrite a tool call before execution (e.g., unwrap meta-tool). */
|
|
40
|
+
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
41
|
+
/** Record the assistant turn in conversation state. */
|
|
42
|
+
recordAssistant(conv: ConversationState, text: string, toolCalls: PendingToolCall[]): void;
|
|
43
|
+
/** Record all tool results for a batch as conversation messages. */
|
|
44
|
+
recordResults(conv: ConversationState, results: ToolResult[]): void;
|
|
45
|
+
/** Create a stream filter for stripping tool calls from display. null = pass-through. */
|
|
46
|
+
createStreamFilter(toolNames: string[]): StreamFilter | null;
|
|
47
|
+
}
|
|
48
|
+
export declare class ApiToolProtocol implements ToolProtocol {
|
|
49
|
+
readonly mode: "api";
|
|
50
|
+
getApiTools(tools: ToolDefinition[]): ChatCompletionTool[] | undefined;
|
|
51
|
+
getToolPrompt(): string;
|
|
52
|
+
extractToolCalls(_text: string, streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
53
|
+
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
54
|
+
recordAssistant(conv: ConversationState, text: string, toolCalls: PendingToolCall[]): void;
|
|
55
|
+
recordResults(conv: ConversationState, results: ToolResult[]): void;
|
|
56
|
+
createStreamFilter(): null;
|
|
57
|
+
}
|
|
58
|
+
export declare class InlineToolProtocol implements ToolProtocol {
|
|
59
|
+
readonly mode: "inline";
|
|
60
|
+
private callCounter;
|
|
61
|
+
getApiTools(): undefined;
|
|
62
|
+
getToolPrompt(tools: ToolDefinition[]): string;
|
|
63
|
+
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
64
|
+
extractToolCalls(text: string, _streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
65
|
+
recordAssistant(conv: ConversationState, text: string, _toolCalls: PendingToolCall[]): void;
|
|
66
|
+
recordResults(conv: ConversationState, results: ToolResult[]): void;
|
|
67
|
+
createStreamFilter(_toolNames: string[]): StreamFilter;
|
|
68
|
+
}
|
|
69
|
+
export declare class DeferredToolProtocol implements ToolProtocol {
|
|
70
|
+
readonly mode: "deferred";
|
|
71
|
+
private coreNames;
|
|
72
|
+
/** Cached extension tool schemas for arg validation. */
|
|
73
|
+
private extSchemas;
|
|
74
|
+
constructor(coreNames: string[]);
|
|
75
|
+
getApiTools(tools: ToolDefinition[]): ChatCompletionTool[] | undefined;
|
|
76
|
+
getToolPrompt(): string;
|
|
77
|
+
extractToolCalls(_text: string, streamedCalls: PendingToolCall[]): PendingToolCall[];
|
|
78
|
+
rewriteToolCall(tc: PendingToolCall): PendingToolCall;
|
|
79
|
+
recordAssistant(conv: ConversationState, text: string, toolCalls: PendingToolCall[]): void;
|
|
80
|
+
recordResults(conv: ConversationState, results: ToolResult[]): void;
|
|
81
|
+
createStreamFilter(): null;
|
|
82
|
+
}
|
|
83
|
+
export declare function createToolProtocol(mode: "api" | "inline" | "deferred"): ToolProtocol;
|