agent-sh 0.3.1 → 0.5.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 +66 -96
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +84 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +45 -2
- package/dist/shell.js +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -100
- package/dist/acp-client.js +0 -656
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the OpenAI chat messages array for the agent loop.
|
|
3
|
+
* Separate from ContextManager — this is the LLM conversation,
|
|
4
|
+
* not the shell history.
|
|
5
|
+
*/
|
|
6
|
+
export class ConversationState {
|
|
7
|
+
messages = [];
|
|
8
|
+
addUserMessage(text) {
|
|
9
|
+
this.messages.push({ role: "user", content: text });
|
|
10
|
+
}
|
|
11
|
+
addAssistantMessage(content, toolCalls) {
|
|
12
|
+
if (toolCalls?.length) {
|
|
13
|
+
this.messages.push({
|
|
14
|
+
role: "assistant",
|
|
15
|
+
content: content ?? null,
|
|
16
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
17
|
+
id: tc.id,
|
|
18
|
+
type: "function",
|
|
19
|
+
function: tc.function,
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.messages.push({ role: "assistant", content: content ?? "" });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
addToolResult(toolCallId, content) {
|
|
28
|
+
this.messages.push({
|
|
29
|
+
role: "tool",
|
|
30
|
+
tool_call_id: toolCallId,
|
|
31
|
+
content,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Inject a system-level note into the conversation (e.g. context change). */
|
|
35
|
+
addSystemNote(text) {
|
|
36
|
+
this.messages.push({ role: "user", content: text });
|
|
37
|
+
}
|
|
38
|
+
getMessages() {
|
|
39
|
+
return this.messages;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Simple compaction — drop oldest turns, keeping the first user message
|
|
43
|
+
* (original task context) and the most recent turns.
|
|
44
|
+
*/
|
|
45
|
+
compact(maxTurns) {
|
|
46
|
+
if (this.messages.length <= maxTurns * 2)
|
|
47
|
+
return;
|
|
48
|
+
const first = this.messages[0];
|
|
49
|
+
const recent = this.messages.slice(-(maxTurns * 2));
|
|
50
|
+
this.messages = [
|
|
51
|
+
first,
|
|
52
|
+
{ role: "user", content: "[Earlier conversation turns omitted for context space]" },
|
|
53
|
+
...recent,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
clear() {
|
|
57
|
+
this.messages = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent backend exports.
|
|
3
|
+
*
|
|
4
|
+
* The default backend is AgentLoop (in-process, OpenAI-compatible API).
|
|
5
|
+
* Extensions can register alternative backends via agent:register-backend.
|
|
6
|
+
*/
|
|
7
|
+
export type { AgentBackend } from "./types.js";
|
|
8
|
+
export type { ToolDefinition, ToolResult, ToolDisplayInfo } from "./types.js";
|
|
9
|
+
export { AgentLoop } from "./agent-loop.js";
|
|
10
|
+
export { ToolRegistry } from "./tool-registry.js";
|
|
11
|
+
export { runSubagent, type SubagentOptions } from "./subagent.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent backend exports.
|
|
3
|
+
*
|
|
4
|
+
* The default backend is AgentLoop (in-process, OpenAI-compatible API).
|
|
5
|
+
* Extensions can register alternative backends via agent:register-backend.
|
|
6
|
+
*/
|
|
7
|
+
export { AgentLoop } from "./agent-loop.js";
|
|
8
|
+
export { ToolRegistry } from "./tool-registry.js";
|
|
9
|
+
export { runSubagent } from "./subagent.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Skill {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
baseDir: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Discover global skills (stable across cwd changes).
|
|
9
|
+
* Default: ~/.agents/skills/, plus any skillPaths from settings.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverGlobalSkills(): Skill[];
|
|
12
|
+
/**
|
|
13
|
+
* Discover project-level skills from .agents/skills/ in cwd hierarchy.
|
|
14
|
+
* Scans from cwd up to git root.
|
|
15
|
+
*/
|
|
16
|
+
export declare function discoverProjectSkills(cwd: string): Skill[];
|
|
17
|
+
/**
|
|
18
|
+
* Discover all skills (global + project).
|
|
19
|
+
*/
|
|
20
|
+
export declare function discoverSkills(cwd: string): Skill[];
|
|
21
|
+
/**
|
|
22
|
+
* Load the full content of a skill (frontmatter stripped).
|
|
23
|
+
* Returns XML-wrapped content suitable for injection into conversation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadSkillContent(skill: Skill): string | null;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill discovery and loading.
|
|
3
|
+
*
|
|
4
|
+
* Follows the Agent Skills standard (agentskills.io):
|
|
5
|
+
* - Skills are directories containing a SKILL.md with YAML frontmatter
|
|
6
|
+
* - Frontmatter must include `name` and `description`
|
|
7
|
+
* - Full content is loaded on-demand (only names/descriptions in system prompt)
|
|
8
|
+
*
|
|
9
|
+
* Discovery locations:
|
|
10
|
+
* Global: ~/.agent-sh/skills/ (default), plus skillPaths from settings
|
|
11
|
+
* Project: .agents/skills/ in cwd and ancestor dirs (up to git root)
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import { getSettings } from "../settings.js";
|
|
17
|
+
/** Parse YAML frontmatter from a SKILL.md file. */
|
|
18
|
+
function parseFrontmatter(content) {
|
|
19
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
20
|
+
if (!match)
|
|
21
|
+
return null;
|
|
22
|
+
const meta = {};
|
|
23
|
+
for (const line of match[1].split("\n")) {
|
|
24
|
+
const colon = line.indexOf(":");
|
|
25
|
+
if (colon > 0) {
|
|
26
|
+
const key = line.slice(0, colon).trim();
|
|
27
|
+
const value = line.slice(colon + 1).trim();
|
|
28
|
+
meta[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { meta, body: match[2] };
|
|
32
|
+
}
|
|
33
|
+
/** Load a single skill from a SKILL.md file. */
|
|
34
|
+
function loadSkillFromFile(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
const parsed = parseFrontmatter(content);
|
|
38
|
+
if (!parsed)
|
|
39
|
+
return null;
|
|
40
|
+
const name = parsed.meta.name;
|
|
41
|
+
const description = parsed.meta.description;
|
|
42
|
+
if (!name || !description)
|
|
43
|
+
return null;
|
|
44
|
+
if (parsed.meta["disable-model-invocation"] === "true")
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
description,
|
|
49
|
+
filePath,
|
|
50
|
+
baseDir: path.dirname(filePath),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Recursively scan a directory for SKILL.md files. */
|
|
58
|
+
function scanDir(dir) {
|
|
59
|
+
const skills = [];
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return skills;
|
|
66
|
+
}
|
|
67
|
+
// If this directory has a SKILL.md, it's a skill root — don't recurse further
|
|
68
|
+
const skillMd = path.join(dir, "SKILL.md");
|
|
69
|
+
try {
|
|
70
|
+
fs.accessSync(skillMd);
|
|
71
|
+
const skill = loadSkillFromFile(skillMd);
|
|
72
|
+
if (skill)
|
|
73
|
+
skills.push(skill);
|
|
74
|
+
return skills;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// No SKILL.md here — check subdirectories
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
81
|
+
continue;
|
|
82
|
+
const fullPath = path.join(dir, entry.name);
|
|
83
|
+
const isDir = entry.isDirectory() ||
|
|
84
|
+
(entry.isSymbolicLink() && (() => { try {
|
|
85
|
+
return fs.statSync(fullPath).isDirectory();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
} })());
|
|
90
|
+
if (isDir) {
|
|
91
|
+
skills.push(...scanDir(fullPath));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return skills;
|
|
95
|
+
}
|
|
96
|
+
/** Find the git root from a directory. */
|
|
97
|
+
function findGitRoot(dir) {
|
|
98
|
+
let current = path.resolve(dir);
|
|
99
|
+
while (true) {
|
|
100
|
+
try {
|
|
101
|
+
fs.accessSync(path.join(current, ".git"));
|
|
102
|
+
return current;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
const parent = path.dirname(current);
|
|
106
|
+
if (parent === current)
|
|
107
|
+
return null;
|
|
108
|
+
current = parent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Expand ~ to home directory. */
|
|
113
|
+
function expandHome(p) {
|
|
114
|
+
if (p.startsWith("~/") || p === "~") {
|
|
115
|
+
return path.join(os.homedir(), p.slice(1));
|
|
116
|
+
}
|
|
117
|
+
return p;
|
|
118
|
+
}
|
|
119
|
+
function addUnique(target, source, seen) {
|
|
120
|
+
for (const skill of source) {
|
|
121
|
+
if (!seen.has(skill.name)) {
|
|
122
|
+
seen.add(skill.name);
|
|
123
|
+
target.push(skill);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Discover global skills (stable across cwd changes).
|
|
129
|
+
* Default: ~/.agents/skills/, plus any skillPaths from settings.
|
|
130
|
+
*/
|
|
131
|
+
export function discoverGlobalSkills() {
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const skills = [];
|
|
134
|
+
addUnique(skills, scanDir(path.join(os.homedir(), ".agent-sh", "skills")), seen);
|
|
135
|
+
const settings = getSettings();
|
|
136
|
+
for (const p of settings.skillPaths ?? []) {
|
|
137
|
+
addUnique(skills, scanDir(path.resolve(expandHome(p))), seen);
|
|
138
|
+
}
|
|
139
|
+
return skills;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Discover project-level skills from .agents/skills/ in cwd hierarchy.
|
|
143
|
+
* Scans from cwd up to git root.
|
|
144
|
+
*/
|
|
145
|
+
export function discoverProjectSkills(cwd) {
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
const skills = [];
|
|
148
|
+
const gitRoot = findGitRoot(cwd);
|
|
149
|
+
let current = path.resolve(cwd);
|
|
150
|
+
while (true) {
|
|
151
|
+
addUnique(skills, scanDir(path.join(current, ".agents", "skills")), seen);
|
|
152
|
+
if (gitRoot && current === gitRoot)
|
|
153
|
+
break;
|
|
154
|
+
const parent = path.dirname(current);
|
|
155
|
+
if (parent === current)
|
|
156
|
+
break;
|
|
157
|
+
current = parent;
|
|
158
|
+
}
|
|
159
|
+
return skills;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Discover all skills (global + project).
|
|
163
|
+
*/
|
|
164
|
+
export function discoverSkills(cwd) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
const skills = [];
|
|
167
|
+
addUnique(skills, discoverGlobalSkills(), seen);
|
|
168
|
+
addUnique(skills, discoverProjectSkills(cwd), seen);
|
|
169
|
+
return skills;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Load the full content of a skill (frontmatter stripped).
|
|
173
|
+
* Returns XML-wrapped content suitable for injection into conversation.
|
|
174
|
+
*/
|
|
175
|
+
export function loadSkillContent(skill) {
|
|
176
|
+
try {
|
|
177
|
+
const content = fs.readFileSync(skill.filePath, "utf-8");
|
|
178
|
+
const parsed = parseFrontmatter(content);
|
|
179
|
+
if (!parsed)
|
|
180
|
+
return content;
|
|
181
|
+
return `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${parsed.body.trim()}\n</skill>`;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent runner — executes a focused agent loop with its own context.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the main AgentLoop, a subagent:
|
|
5
|
+
* - Has its own conversation (starts fresh, stays focused)
|
|
6
|
+
* - Has its own system prompt (specialized for the task)
|
|
7
|
+
* - Runs to completion and returns the final text
|
|
8
|
+
* - Optionally emits tool events to the bus for TUI rendering
|
|
9
|
+
*
|
|
10
|
+
* Used by the subagent extension to delegate tasks from the main agent.
|
|
11
|
+
*/
|
|
12
|
+
import type { EventBus } from "../event-bus.js";
|
|
13
|
+
import type { LlmClient } from "../utils/llm-client.js";
|
|
14
|
+
import type { ToolDefinition } from "./types.js";
|
|
15
|
+
export interface SubagentOptions {
|
|
16
|
+
/** LLM client to use. */
|
|
17
|
+
llmClient: LlmClient;
|
|
18
|
+
/** Tools available to the subagent. */
|
|
19
|
+
tools: ToolDefinition[];
|
|
20
|
+
/** System prompt for this subagent. */
|
|
21
|
+
systemPrompt: string;
|
|
22
|
+
/** The task to perform. */
|
|
23
|
+
task: string;
|
|
24
|
+
/** Model override (optional, defaults to llmClient's model). */
|
|
25
|
+
model?: string;
|
|
26
|
+
/** Event bus for TUI events (optional — silent if omitted). */
|
|
27
|
+
bus?: EventBus;
|
|
28
|
+
/** Abort signal for cancellation. */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
/** Max tool loop iterations (default 20). */
|
|
31
|
+
maxIterations?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run a subagent to completion.
|
|
35
|
+
* Returns the final response text.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runSubagent(opts: SubagentOptions): Promise<string>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ConversationState } from "./conversation-state.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run a subagent to completion.
|
|
4
|
+
* Returns the final response text.
|
|
5
|
+
*/
|
|
6
|
+
export async function runSubagent(opts) {
|
|
7
|
+
const { llmClient, tools, systemPrompt, task, model, bus, signal, maxIterations = 20, } = opts;
|
|
8
|
+
const toolMap = new Map(tools.map(t => [t.name, t]));
|
|
9
|
+
const apiTools = tools.map(t => ({
|
|
10
|
+
type: "function",
|
|
11
|
+
function: {
|
|
12
|
+
name: t.name,
|
|
13
|
+
description: t.description,
|
|
14
|
+
parameters: t.input_schema,
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
const conversation = new ConversationState();
|
|
18
|
+
conversation.addUserMessage(task);
|
|
19
|
+
let fullResponseText = "";
|
|
20
|
+
let iterations = 0;
|
|
21
|
+
while (iterations++ < maxIterations) {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
break;
|
|
24
|
+
// Stream LLM response
|
|
25
|
+
const { text, toolCalls, assistantContent, assistantToolCalls } = await streamOnce(llmClient, systemPrompt, conversation, apiTools, model, signal);
|
|
26
|
+
fullResponseText += text;
|
|
27
|
+
conversation.addAssistantMessage(assistantContent, assistantToolCalls);
|
|
28
|
+
// No tool calls → done
|
|
29
|
+
if (toolCalls.length === 0)
|
|
30
|
+
break;
|
|
31
|
+
// Execute tools
|
|
32
|
+
for (const tc of toolCalls) {
|
|
33
|
+
if (signal?.aborted)
|
|
34
|
+
break;
|
|
35
|
+
const tool = toolMap.get(tc.name);
|
|
36
|
+
if (!tool) {
|
|
37
|
+
conversation.addToolResult(tc.id, `Error: Unknown tool "${tc.name}"`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
let args;
|
|
41
|
+
try {
|
|
42
|
+
args = JSON.parse(tc.argumentsJson);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
conversation.addToolResult(tc.id, `Error: Invalid JSON arguments for ${tc.name}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Emit tool events for TUI (if bus provided)
|
|
49
|
+
if (bus) {
|
|
50
|
+
const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
|
|
51
|
+
bus.emit("agent:tool-started", {
|
|
52
|
+
title: tc.name,
|
|
53
|
+
toolCallId: tc.id,
|
|
54
|
+
kind: display.kind,
|
|
55
|
+
locations: display.locations,
|
|
56
|
+
rawInput: args,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const onChunk = bus && tool.showOutput !== false
|
|
60
|
+
? (chunk) => { bus.emit("agent:tool-output-chunk", { chunk }); }
|
|
61
|
+
: undefined;
|
|
62
|
+
const result = await tool.execute(args, onChunk);
|
|
63
|
+
if (bus) {
|
|
64
|
+
const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
|
|
65
|
+
bus.emit("agent:tool-completed", {
|
|
66
|
+
toolCallId: tc.id,
|
|
67
|
+
exitCode: result.exitCode,
|
|
68
|
+
rawOutput: result.content,
|
|
69
|
+
kind: display.kind,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const content = result.isError ? `Error: ${result.content}` : result.content;
|
|
73
|
+
conversation.addToolResult(tc.id, content);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return fullResponseText;
|
|
77
|
+
}
|
|
78
|
+
/** Stream a single LLM response. */
|
|
79
|
+
async function streamOnce(llmClient, systemPrompt, conversation, apiTools, model, signal) {
|
|
80
|
+
let text = "";
|
|
81
|
+
const pendingToolCalls = [];
|
|
82
|
+
const stream = await llmClient.stream({
|
|
83
|
+
messages: [
|
|
84
|
+
{ role: "system", content: systemPrompt },
|
|
85
|
+
...conversation.getMessages(),
|
|
86
|
+
],
|
|
87
|
+
tools: apiTools.length > 0 ? apiTools : undefined,
|
|
88
|
+
model,
|
|
89
|
+
signal,
|
|
90
|
+
});
|
|
91
|
+
for await (const chunk of stream) {
|
|
92
|
+
if (signal?.aborted)
|
|
93
|
+
break;
|
|
94
|
+
const choice = chunk.choices[0];
|
|
95
|
+
if (!choice)
|
|
96
|
+
continue;
|
|
97
|
+
const delta = choice.delta;
|
|
98
|
+
if (delta?.content) {
|
|
99
|
+
text += delta.content;
|
|
100
|
+
}
|
|
101
|
+
if (delta?.tool_calls) {
|
|
102
|
+
for (const tc of delta.tool_calls) {
|
|
103
|
+
const idx = tc.index;
|
|
104
|
+
if (!pendingToolCalls[idx]) {
|
|
105
|
+
pendingToolCalls[idx] = { id: tc.id, name: tc.function.name, argumentsJson: "" };
|
|
106
|
+
}
|
|
107
|
+
if (tc.function?.arguments) {
|
|
108
|
+
pendingToolCalls[idx].argumentsJson += tc.function.arguments;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const assistantToolCalls = pendingToolCalls.length
|
|
114
|
+
? pendingToolCalls.map(tc => ({ id: tc.id, function: { name: tc.name, arguments: tc.argumentsJson } }))
|
|
115
|
+
: undefined;
|
|
116
|
+
return { text, toolCalls: pendingToolCalls, assistantContent: text || null, assistantToolCalls };
|
|
117
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
import type { ContextManager } from "../context-manager.js";
|
|
3
|
+
/**
|
|
4
|
+
* Static system prompt — identical across all queries, cacheable.
|
|
5
|
+
* Contains only identity and behavioral instructions.
|
|
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# Input Modes\n\nThe user interacts with you through two modes:\n\nEXECUTE mode (triggered by '>'): The user is asking questions or requesting tasks.\nUse your internal tools (bash, file operations, etc.) to accomplish tasks.\nDo NOT use user_shell in this mode unless the user explicitly asks to run\nsomething in their live shell.\n\nHELP mode (triggered by '?'): The user wants a command run in their live shell.\nYou may use your tools to investigate first (read files, grep, etc.), but the\nfinal action must be running the command via user_shell with return_output=false.\nThe user sees the output directly \u2014 you don't need to see or summarize it.\nDo not explain, confirm, or comment on the result \u2014 just run it and stop.\n\nEach prompt includes a per-query mode instruction \u2014 follow it.\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\n- user_shell runs commands in the user's live terminal \u2014 use for cd, export, source, etc.\n- user_shell output is shown directly to the user but NOT returned to you by default.\n Set return_output=true if you need to inspect the result to answer a question.";
|
|
8
|
+
/**
|
|
9
|
+
* Build the dynamic context — injected as a user message before each query.
|
|
10
|
+
* Contains everything that changes: tools, shell context, conventions, cwd.
|
|
11
|
+
*
|
|
12
|
+
* Runs through the "agent:dynamic-context" pipe so extensions can append.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDynamicContext(tools: ToolDefinition[], contextManager: ContextManager): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { discoverSkills } from "./skills.js";
|
|
4
|
+
/** File names to scan for project conventions (checked in order). */
|
|
5
|
+
const CONVENTION_FILES = ["CLAUDE.md", "AGENT.md"];
|
|
6
|
+
/**
|
|
7
|
+
* Scan from `dir` upward for project convention files.
|
|
8
|
+
* Returns contents ordered root-first (general → specific).
|
|
9
|
+
*/
|
|
10
|
+
function loadConventionFiles(dir) {
|
|
11
|
+
const files = [];
|
|
12
|
+
let current = path.resolve(dir);
|
|
13
|
+
while (true) {
|
|
14
|
+
for (const name of CONVENTION_FILES) {
|
|
15
|
+
const candidate = path.join(current, name);
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(candidate, "utf-8").trim();
|
|
18
|
+
if (content) {
|
|
19
|
+
files.push({ path: candidate, content });
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// File doesn't exist
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(current);
|
|
28
|
+
if (parent === current)
|
|
29
|
+
break;
|
|
30
|
+
current = parent;
|
|
31
|
+
}
|
|
32
|
+
files.reverse();
|
|
33
|
+
return files.map(f => `<!-- ${f.path} -->\n${f.content}`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Static system prompt — identical across all queries, cacheable.
|
|
37
|
+
* Contains only identity and behavioral instructions.
|
|
38
|
+
*/
|
|
39
|
+
export const STATIC_SYSTEM_PROMPT = `You are an AI coding assistant embedded in agent-sh, a terminal shell.
|
|
40
|
+
You have access to the user's shell environment and can read, write, and execute code.
|
|
41
|
+
You share the user's working directory, environment variables, and shell history.
|
|
42
|
+
|
|
43
|
+
# Input Modes
|
|
44
|
+
|
|
45
|
+
The user interacts with you through two modes:
|
|
46
|
+
|
|
47
|
+
EXECUTE mode (triggered by '>'): The user is asking questions or requesting tasks.
|
|
48
|
+
Use your internal tools (bash, file operations, etc.) to accomplish tasks.
|
|
49
|
+
Do NOT use user_shell in this mode unless the user explicitly asks to run
|
|
50
|
+
something in their live shell.
|
|
51
|
+
|
|
52
|
+
HELP mode (triggered by '?'): The user wants a command run in their live shell.
|
|
53
|
+
You may use your tools to investigate first (read files, grep, etc.), but the
|
|
54
|
+
final action must be running the command via user_shell with return_output=false.
|
|
55
|
+
The user sees the output directly — you don't need to see or summarize it.
|
|
56
|
+
Do not explain, confirm, or comment on the result — just run it and stop.
|
|
57
|
+
|
|
58
|
+
Each prompt includes a per-query mode instruction — follow it.
|
|
59
|
+
|
|
60
|
+
# Tool Usage Guidelines
|
|
61
|
+
- Use read_file before editing a file you haven't seen
|
|
62
|
+
- Prefer edit_file over write_file for modifying existing files
|
|
63
|
+
- Use grep/glob to find files before reading them
|
|
64
|
+
- Keep bash commands focused; avoid long-running blocking commands
|
|
65
|
+
- Always check command exit codes for errors
|
|
66
|
+
- user_shell runs commands in the user's live terminal — use for cd, export, source, etc.
|
|
67
|
+
- user_shell output is shown directly to the user but NOT returned to you by default.
|
|
68
|
+
Set return_output=true if you need to inspect the result to answer a question.`;
|
|
69
|
+
/**
|
|
70
|
+
* Build the dynamic context — injected as a user message before each query.
|
|
71
|
+
* Contains everything that changes: tools, shell context, conventions, cwd.
|
|
72
|
+
*
|
|
73
|
+
* Runs through the "agent:dynamic-context" pipe so extensions can append.
|
|
74
|
+
*/
|
|
75
|
+
export function buildDynamicContext(tools, contextManager) {
|
|
76
|
+
const sections = [];
|
|
77
|
+
// Tools
|
|
78
|
+
sections.push("# Available Tools\n" +
|
|
79
|
+
tools.map((t) => `- ${t.name}: ${t.description}`).join("\n"));
|
|
80
|
+
// Project conventions (CLAUDE.md / AGENT.md)
|
|
81
|
+
const conventions = loadConventionFiles(contextManager.getCwd());
|
|
82
|
+
if (conventions.length > 0) {
|
|
83
|
+
sections.push("# Project Conventions\n\n" + conventions.join("\n\n"));
|
|
84
|
+
}
|
|
85
|
+
// Skills hint
|
|
86
|
+
const skills = discoverSkills(contextManager.getCwd());
|
|
87
|
+
if (skills.length > 0) {
|
|
88
|
+
sections.push(`You have access to ${skills.length} skill(s). Use the list_skills tool to see them, then read_file to load one.`);
|
|
89
|
+
}
|
|
90
|
+
// Shell context
|
|
91
|
+
const shellContext = contextManager.getContext();
|
|
92
|
+
if (shellContext) {
|
|
93
|
+
sections.push(shellContext);
|
|
94
|
+
}
|
|
95
|
+
// Metadata
|
|
96
|
+
sections.push(`Current date: ${new Date().toISOString().split("T")[0]}\nWorking directory: ${contextManager.getCwd()}`);
|
|
97
|
+
return sections.join("\n\n");
|
|
98
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
import type { ChatCompletionTool } from "../utils/llm-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registry for agent tools. Holds tool definitions and converts them
|
|
5
|
+
* to OpenAI-compatible function schemas for API calls.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ToolRegistry {
|
|
8
|
+
private tools;
|
|
9
|
+
register(tool: ToolDefinition): void;
|
|
10
|
+
unregister(name: string): void;
|
|
11
|
+
get(name: string): ToolDefinition | undefined;
|
|
12
|
+
all(): ToolDefinition[];
|
|
13
|
+
/** Convert to OpenAI-compatible tool schemas for API calls. */
|
|
14
|
+
toAPITools(): ChatCompletionTool[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for agent tools. Holds tool definitions and converts them
|
|
3
|
+
* to OpenAI-compatible function schemas for API calls.
|
|
4
|
+
*/
|
|
5
|
+
export class ToolRegistry {
|
|
6
|
+
tools = new Map();
|
|
7
|
+
register(tool) {
|
|
8
|
+
this.tools.set(tool.name, tool);
|
|
9
|
+
}
|
|
10
|
+
unregister(name) {
|
|
11
|
+
this.tools.delete(name);
|
|
12
|
+
}
|
|
13
|
+
get(name) {
|
|
14
|
+
return this.tools.get(name);
|
|
15
|
+
}
|
|
16
|
+
all() {
|
|
17
|
+
return Array.from(this.tools.values());
|
|
18
|
+
}
|
|
19
|
+
/** Convert to OpenAI-compatible tool schemas for API calls. */
|
|
20
|
+
toAPITools() {
|
|
21
|
+
return this.all().map((t) => ({
|
|
22
|
+
type: "function",
|
|
23
|
+
function: {
|
|
24
|
+
name: t.name,
|
|
25
|
+
description: t.description,
|
|
26
|
+
parameters: t.input_schema,
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { executeCommand } from "../../executor.js";
|
|
2
|
+
export function createBashTool(opts) {
|
|
3
|
+
return {
|
|
4
|
+
name: "bash",
|
|
5
|
+
description: "Execute a bash command in an isolated subprocess. Output is captured and returned to you. Does not affect the user's shell state.",
|
|
6
|
+
input_schema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
command: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "The bash command to execute",
|
|
12
|
+
},
|
|
13
|
+
timeout: {
|
|
14
|
+
type: "number",
|
|
15
|
+
description: "Timeout in seconds (default: 60)",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["command"],
|
|
19
|
+
},
|
|
20
|
+
showOutput: true,
|
|
21
|
+
modifiesFiles: true,
|
|
22
|
+
requiresPermission: true,
|
|
23
|
+
getDisplayInfo: (args) => ({
|
|
24
|
+
kind: "execute",
|
|
25
|
+
locations: [],
|
|
26
|
+
}),
|
|
27
|
+
async execute(args, onChunk) {
|
|
28
|
+
const command = args.command;
|
|
29
|
+
const timeout = (args.timeout ?? 60) * 1000;
|
|
30
|
+
// Let extensions intercept before execution
|
|
31
|
+
const intercepted = opts.bus.emitPipe("agent:terminal-intercept", {
|
|
32
|
+
command,
|
|
33
|
+
cwd: opts.getCwd(),
|
|
34
|
+
intercepted: false,
|
|
35
|
+
output: "",
|
|
36
|
+
});
|
|
37
|
+
if (intercepted.intercepted) {
|
|
38
|
+
return {
|
|
39
|
+
content: intercepted.output,
|
|
40
|
+
exitCode: 0,
|
|
41
|
+
isError: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const { session, done } = executeCommand({
|
|
45
|
+
command,
|
|
46
|
+
cwd: opts.getCwd(),
|
|
47
|
+
env: opts.getEnv(),
|
|
48
|
+
timeout,
|
|
49
|
+
onOutput: onChunk,
|
|
50
|
+
});
|
|
51
|
+
await done;
|
|
52
|
+
const content = session.truncated
|
|
53
|
+
? `[output truncated, showing last portion]\n${session.output}`
|
|
54
|
+
: session.output;
|
|
55
|
+
return {
|
|
56
|
+
content: content || "(no output)",
|
|
57
|
+
exitCode: session.exitCode,
|
|
58
|
+
isError: session.exitCode !== 0,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|