agent-sh 0.12.12 → 0.12.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-loop.d.ts +0 -4
- package/dist/agent/agent-loop.js +27 -39
- package/dist/agent/skills.d.ts +3 -1
- package/dist/agent/skills.js +5 -19
- package/dist/agent/subagent.js +5 -8
- package/dist/agent/system-prompt.d.ts +0 -27
- package/dist/agent/system-prompt.js +5 -24
- package/dist/core.d.ts +3 -4
- package/dist/core.js +30 -27
- package/dist/executor.d.ts +4 -0
- package/dist/executor.js +20 -2
- package/dist/extension-loader.js +7 -0
- package/dist/extensions/agent-backend.js +0 -1
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/file-autocomplete.js +3 -3
- package/dist/extensions/index.d.ts +4 -0
- package/dist/extensions/index.js +1 -0
- package/dist/extensions/shell-context.d.ts +7 -0
- package/dist/extensions/shell-context.js +129 -0
- package/dist/extensions/slash-commands.js +2 -2
- package/dist/index.js +15 -16
- package/dist/shell/index.d.ts +35 -0
- package/dist/shell/index.js +47 -0
- package/dist/types.d.ts +26 -32
- package/dist/utils/message-utils.d.ts +13 -0
- package/dist/utils/message-utils.js +26 -0
- package/examples/extensions/overlay-agent.ts +9 -3
- package/examples/extensions/peer-mesh.ts +10 -7
- package/examples/extensions/subagents.ts +2 -2
- package/examples/extensions/terminal-buffer.ts +3 -2
- package/examples/extensions/tmux-pane.ts +5 -1
- package/examples/extensions/user-shell.ts +1 -1
- package/package.json +1 -1
- package/dist/context-manager.d.ts +0 -45
- package/dist/context-manager.js +0 -242
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { EventBus } from "../event-bus.js";
|
|
15
15
|
import type { AgentMode } from "../types.js";
|
|
16
|
-
import type { ContextManager } from "../context-manager.js";
|
|
17
16
|
import type { LlmClient } from "../utils/llm-client.js";
|
|
18
17
|
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
19
18
|
import type { AgentBackend, ToolDefinition } from "./types.js";
|
|
@@ -21,7 +20,6 @@ import { type HistoryAdapter } from "./history-file.js";
|
|
|
21
20
|
import type { Compositor } from "../utils/compositor.js";
|
|
22
21
|
export interface AgentLoopConfig {
|
|
23
22
|
bus: EventBus;
|
|
24
|
-
contextManager: ContextManager;
|
|
25
23
|
llmClient: LlmClient;
|
|
26
24
|
handlers: HandlerFunctions;
|
|
27
25
|
modes?: AgentMode[];
|
|
@@ -57,14 +55,12 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
57
55
|
private lastErrorByFile;
|
|
58
56
|
private static readonly THINKING_LEVELS;
|
|
59
57
|
private bus;
|
|
60
|
-
private contextManager;
|
|
61
58
|
private llmClient;
|
|
62
59
|
private handlers;
|
|
63
60
|
private thinkingLevel;
|
|
64
61
|
private compositor;
|
|
65
62
|
private toolProtocol;
|
|
66
63
|
private instanceId;
|
|
67
|
-
private lastShellSeq;
|
|
68
64
|
constructor(config: AgentLoopConfig);
|
|
69
65
|
/** Subscribe to bus events — activates this backend. */
|
|
70
66
|
wire(): void;
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -7,15 +7,17 @@ import { ToolRegistry } from "./tool-registry.js";
|
|
|
7
7
|
import { ConversationState } from "./conversation-state.js";
|
|
8
8
|
import { HistoryFile } from "./history-file.js";
|
|
9
9
|
import { nucleate, formatNuclearLine, isReadOnly } from "./nuclear-form.js";
|
|
10
|
-
import { STATIC_SYSTEM_PROMPT,
|
|
10
|
+
import { STATIC_SYSTEM_PROMPT, buildStaticByCwd, formatSkillsBlock, loadGlobalAgentsMd } from "./system-prompt.js";
|
|
11
11
|
import { createToolUI } from "../utils/tool-interactive.js";
|
|
12
12
|
import { RESPONSE_RESERVE, DEFAULT_CONTEXT_WINDOW } from "./token-budget.js";
|
|
13
13
|
import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
14
|
+
import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
|
|
14
15
|
import { getSettings, updateSettings } from "../settings.js";
|
|
15
16
|
import { createToolProtocol } from "./tool-protocol.js";
|
|
16
17
|
// Core tool factories
|
|
17
18
|
import { createBashTool } from "./tools/bash.js";
|
|
18
19
|
import { createPwshTool } from "./tools/pwsh.js";
|
|
20
|
+
import { findBash } from "../executor.js";
|
|
19
21
|
import { createReadFileTool } from "./tools/read-file.js";
|
|
20
22
|
import { createWriteFileTool } from "./tools/write-file.js";
|
|
21
23
|
import { createEditFileTool } from "./tools/edit-file.js";
|
|
@@ -88,20 +90,14 @@ export class AgentLoop {
|
|
|
88
90
|
lastErrorByFile = new Map(); // file path → error summary
|
|
89
91
|
static THINKING_LEVELS = ["off", "low", "medium", "high"];
|
|
90
92
|
bus;
|
|
91
|
-
contextManager;
|
|
92
93
|
llmClient;
|
|
93
94
|
handlers;
|
|
94
95
|
thinkingLevel = "off";
|
|
95
96
|
compositor = null;
|
|
96
97
|
toolProtocol;
|
|
97
98
|
instanceId;
|
|
98
|
-
// Cursor into ContextManager's exchange stream. Events with id > this
|
|
99
|
-
// have not yet been shown to the LLM. We inject the delta as a user
|
|
100
|
-
// message before each stream so the prefix stays cacheable.
|
|
101
|
-
lastShellSeq = 0;
|
|
102
99
|
constructor(config) {
|
|
103
100
|
this.bus = config.bus;
|
|
104
|
-
this.contextManager = config.contextManager;
|
|
105
101
|
this.llmClient = config.llmClient;
|
|
106
102
|
this.handlers = config.handlers;
|
|
107
103
|
this.compositor = config.compositor ?? null;
|
|
@@ -597,7 +593,7 @@ export class AgentLoop {
|
|
|
597
593
|
return `${raw}${context}`;
|
|
598
594
|
}
|
|
599
595
|
registerCoreTools() {
|
|
600
|
-
const getCwd = () => this.
|
|
596
|
+
const getCwd = () => this.handlers.call("cwd");
|
|
601
597
|
const getEnv = () => {
|
|
602
598
|
const env = {};
|
|
603
599
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -606,7 +602,9 @@ export class AgentLoop {
|
|
|
606
602
|
}
|
|
607
603
|
return env;
|
|
608
604
|
};
|
|
609
|
-
|
|
605
|
+
if (findBash() !== null) {
|
|
606
|
+
this.toolRegistry.register(createBashTool({ getCwd, getEnv, bus: this.bus }));
|
|
607
|
+
}
|
|
610
608
|
if (process.platform === "win32") {
|
|
611
609
|
this.toolRegistry.register(createPwshTool({ getCwd, getEnv, bus: this.bus }));
|
|
612
610
|
}
|
|
@@ -714,7 +712,7 @@ export class AgentLoop {
|
|
|
714
712
|
// Placed here so they enter the provider's prompt cache with the
|
|
715
713
|
// system prompt, and only re-materialize when cwd changes invalidate
|
|
716
714
|
// cachedSystemPrompt in executeLoop.
|
|
717
|
-
const projectStatic = buildStaticByCwd(this.
|
|
715
|
+
const projectStatic = buildStaticByCwd(this.handlers.call("cwd"));
|
|
718
716
|
if (projectStatic)
|
|
719
717
|
parts.push(projectStatic);
|
|
720
718
|
// Extension sections (tools, skills, instructions grouped by extension)
|
|
@@ -790,12 +788,9 @@ export class AgentLoop {
|
|
|
790
788
|
};
|
|
791
789
|
});
|
|
792
790
|
h.define("agent:get-self", () => this);
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const promptTokens = this.conversation.estimatePromptTokens();
|
|
797
|
-
return buildDynamicContext(this.contextManager, { promptTokens, contextWindow });
|
|
798
|
-
});
|
|
791
|
+
// dynamic-context:build / query-context:build are defined in core.ts.
|
|
792
|
+
// ash consumes them via the envelope wrapping in streamResponse +
|
|
793
|
+
// handleQuery; other backends may ignore.
|
|
799
794
|
// Full control over what the LLM sees: takes messages[], returns messages[].
|
|
800
795
|
// Default: pass through. Extensions can advise to compact, summarize,
|
|
801
796
|
// filter, reorder, inject — whatever strategy fits.
|
|
@@ -1029,14 +1024,14 @@ export class AgentLoop {
|
|
|
1029
1024
|
this.bus.emit("agent:processing-start", {});
|
|
1030
1025
|
let responseText = "";
|
|
1031
1026
|
try {
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
const
|
|
1037
|
-
const userContent =
|
|
1038
|
-
|
|
1039
|
-
|
|
1027
|
+
// Per-query producers (shell events + any extension-registered
|
|
1028
|
+
// per-query signals) produce content that gets frozen into this
|
|
1029
|
+
// user message inside <query_context>, distinguishing it from the
|
|
1030
|
+
// per-request <dynamic_context> wrapped on the trailing message.
|
|
1031
|
+
const queryContext = (this.handlers.call("query-context:build") ?? "").trim();
|
|
1032
|
+
const userContent = queryContext
|
|
1033
|
+
? `<query_context>\n${queryContext}\n</query_context>\n\n${query}`
|
|
1034
|
+
: query;
|
|
1040
1035
|
this.conversation.addUserMessage(userContent);
|
|
1041
1036
|
this.bus.emit("conversation:message-appended", { role: "user", content: query });
|
|
1042
1037
|
responseText = await this.executeLoop(signal);
|
|
@@ -1080,7 +1075,7 @@ export class AgentLoop {
|
|
|
1080
1075
|
// so live signals (budget, in-flight subagents, metacognitive warnings)
|
|
1081
1076
|
// are fresh.
|
|
1082
1077
|
let cachedSystemPrompt;
|
|
1083
|
-
let lastCwd = this.
|
|
1078
|
+
let lastCwd = this.handlers.call("cwd");
|
|
1084
1079
|
while (!signal.aborted) {
|
|
1085
1080
|
// Auto-compact when total context approaches the window limit.
|
|
1086
1081
|
const totalEstimate = this.conversation.estimatePromptTokens();
|
|
@@ -1101,7 +1096,7 @@ export class AgentLoop {
|
|
|
1101
1096
|
}
|
|
1102
1097
|
cachedSystemPrompt = undefined;
|
|
1103
1098
|
}
|
|
1104
|
-
const currentCwd = this.
|
|
1099
|
+
const currentCwd = this.handlers.call("cwd");
|
|
1105
1100
|
if (currentCwd !== lastCwd) {
|
|
1106
1101
|
cachedSystemPrompt = undefined;
|
|
1107
1102
|
lastCwd = currentCwd;
|
|
@@ -1516,24 +1511,17 @@ export class AgentLoop {
|
|
|
1516
1511
|
let reasoning = "";
|
|
1517
1512
|
const reasoningDetailsByIndex = new Map();
|
|
1518
1513
|
const pendingToolCalls = [];
|
|
1514
|
+
// Tool protocol controls what goes in the API tools param vs dynamic context
|
|
1515
|
+
const apiTools = this.toolProtocol.getApiTools(this.toolRegistry.all());
|
|
1516
|
+
const toolPrompt = this.toolProtocol.getToolPrompt(this.toolRegistry.all());
|
|
1517
|
+
// Dynamic context rides on the trailing message — see
|
|
1518
|
+
// wrapTrailingWithDynamicContext for the cache-stability rationale.
|
|
1519
1519
|
const rawMessages = [
|
|
1520
1520
|
{ role: "system", content: systemPrompt },
|
|
1521
|
-
|
|
1522
|
-
{ role: "assistant", content: "Understood." },
|
|
1523
|
-
...this.conversation.getMessages(),
|
|
1521
|
+
...wrapTrailingWithDynamicContext(this.conversation.getMessages(), dynamicContext, toolPrompt),
|
|
1524
1522
|
];
|
|
1525
1523
|
// Let extensions transform the message array (compact, summarize, filter, etc.)
|
|
1526
1524
|
const messages = this.handlers.call("conversation:prepare", rawMessages);
|
|
1527
|
-
// Tool protocol controls what goes in the API tools param vs dynamic context
|
|
1528
|
-
const apiTools = this.toolProtocol.getApiTools(this.toolRegistry.all());
|
|
1529
|
-
const toolPrompt = this.toolProtocol.getToolPrompt(this.toolRegistry.all());
|
|
1530
|
-
// Append tool catalog to dynamic context (closer to user query = better followed)
|
|
1531
|
-
if (toolPrompt) {
|
|
1532
|
-
const ctxMsg = messages[1]; // dynamic context user message
|
|
1533
|
-
if (ctxMsg && typeof ctxMsg.content === "string") {
|
|
1534
|
-
ctxMsg.content += "\n" + toolPrompt;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
1525
|
// Stream filter strips tool tags from display (inline mode only)
|
|
1538
1526
|
const streamFilter = this.toolProtocol.createStreamFilter(this.toolRegistry.all().map((t) => t.name));
|
|
1539
1527
|
const requestParams = {
|
package/dist/agent/skills.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export declare function discoverGlobalSkills(): Skill[];
|
|
|
9
9
|
export declare function invalidateGlobalSkillsCache(): void;
|
|
10
10
|
/**
|
|
11
11
|
* Discover project-level skills from .agents/skills/ in cwd hierarchy.
|
|
12
|
-
*
|
|
12
|
+
* Walks from cwd up to $HOME (or filesystem root if cwd is outside HOME).
|
|
13
|
+
* Git boundaries are ignored — nested repos under a skills-bearing parent
|
|
14
|
+
* would otherwise hide the parent's skills.
|
|
13
15
|
*/
|
|
14
16
|
export declare function discoverProjectSkills(cwd: string): Skill[];
|
|
15
17
|
/**
|
package/dist/agent/skills.js
CHANGED
|
@@ -93,22 +93,6 @@ function scanDir(dir) {
|
|
|
93
93
|
}
|
|
94
94
|
return skills;
|
|
95
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
96
|
/** Expand ~ to home directory. */
|
|
113
97
|
function expandHome(p) {
|
|
114
98
|
if (p.startsWith("~/") || p === "~") {
|
|
@@ -146,16 +130,18 @@ export function invalidateGlobalSkillsCache() {
|
|
|
146
130
|
}
|
|
147
131
|
/**
|
|
148
132
|
* Discover project-level skills from .agents/skills/ in cwd hierarchy.
|
|
149
|
-
*
|
|
133
|
+
* Walks from cwd up to $HOME (or filesystem root if cwd is outside HOME).
|
|
134
|
+
* Git boundaries are ignored — nested repos under a skills-bearing parent
|
|
135
|
+
* would otherwise hide the parent's skills.
|
|
150
136
|
*/
|
|
151
137
|
export function discoverProjectSkills(cwd) {
|
|
152
138
|
const seen = new Set();
|
|
153
139
|
const skills = [];
|
|
154
|
-
const
|
|
140
|
+
const home = path.resolve(os.homedir());
|
|
155
141
|
let current = path.resolve(cwd);
|
|
156
142
|
while (true) {
|
|
157
143
|
addUnique(skills, scanDir(path.join(current, ".agents", "skills")), seen);
|
|
158
|
-
if (
|
|
144
|
+
if (current === home)
|
|
159
145
|
break;
|
|
160
146
|
const parent = path.dirname(current);
|
|
161
147
|
if (parent === current)
|
package/dist/agent/subagent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConversationState } from "./conversation-state.js";
|
|
2
|
+
import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* Run a subagent to completion.
|
|
4
5
|
* Returns the final response text.
|
|
@@ -99,15 +100,11 @@ async function streamOnce(llmClient, systemPrompt, conversation, apiTools, model
|
|
|
99
100
|
const reasoningDetailsByIndex = new Map();
|
|
100
101
|
const pendingToolCalls = [];
|
|
101
102
|
let usage = null;
|
|
102
|
-
const messages = [
|
|
103
|
-
{ role: "system", content: systemPrompt },
|
|
104
|
-
];
|
|
105
|
-
if (dynamicContext) {
|
|
106
|
-
messages.push({ role: "user", content: `<context>\n${dynamicContext}\n</context>` });
|
|
107
|
-
messages.push({ role: "assistant", content: "Understood." });
|
|
108
|
-
}
|
|
109
103
|
const stream = await llmClient.stream({
|
|
110
|
-
messages: [
|
|
104
|
+
messages: [
|
|
105
|
+
{ role: "system", content: systemPrompt },
|
|
106
|
+
...wrapTrailingWithDynamicContext(conversation.getMessages(), dynamicContext ?? ""),
|
|
107
|
+
],
|
|
111
108
|
tools: apiTools.length > 0 ? apiTools : undefined,
|
|
112
109
|
model,
|
|
113
110
|
signal,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ContextManager } from "../context-manager.js";
|
|
2
1
|
import { type Skill } from "./skills.js";
|
|
3
2
|
/**
|
|
4
3
|
* Format skills for inline display in prompt.
|
|
@@ -12,35 +11,9 @@ export declare function loadGlobalAgentsMd(): string | null;
|
|
|
12
11
|
* Contains only identity and behavioral instructions.
|
|
13
12
|
*/
|
|
14
13
|
export declare const STATIC_SYSTEM_PROMPT: string;
|
|
15
|
-
/**
|
|
16
|
-
* Build the dynamic context — injected as a user message before each query.
|
|
17
|
-
* Contains everything that changes: shell context, conventions, cwd.
|
|
18
|
-
*
|
|
19
|
-
* Runs through the "dynamic-context:build" handler so extensions can advise.
|
|
20
|
-
*/
|
|
21
|
-
export interface TokenStatus {
|
|
22
|
-
/** Estimated prompt tokens (API-grounded when available, else chars/4). */
|
|
23
|
-
promptTokens: number;
|
|
24
|
-
/** Model's context window in tokens. */
|
|
25
|
-
contextWindow: number;
|
|
26
|
-
}
|
|
27
14
|
/**
|
|
28
15
|
* CWD-scoped static context: project conventions (CLAUDE.md / AGENT.md)
|
|
29
16
|
* and discovered skills. Stable for a given cwd — callers should cache
|
|
30
17
|
* on cwd identity rather than rebuilding per LLM iteration.
|
|
31
18
|
*/
|
|
32
19
|
export declare function buildStaticByCwd(cwd: string): string;
|
|
33
|
-
/**
|
|
34
|
-
* Per-iteration dynamic context: date, working directory, token usage.
|
|
35
|
-
* Rebuilt every LLM call. Extension advisors add more sections (budget,
|
|
36
|
-
* subagents, metacognitive signals, etc.) on top.
|
|
37
|
-
*
|
|
38
|
-
* Skills, AGENTS.md, and project conventions live in the system prompt
|
|
39
|
-
* (see `system-prompt:build` in agent-loop) so they enter the provider's
|
|
40
|
-
* prefix cache instead of being rebuilt and re-sent every turn.
|
|
41
|
-
*
|
|
42
|
-
* Shell context is likewise not injected here — it flows into the
|
|
43
|
-
* conversation as incremental <shell-events> messages (see
|
|
44
|
-
* AgentLoop.injectShellDelta) for the same reason.
|
|
45
|
-
*/
|
|
46
|
-
export declare function buildDynamicContext(contextManager: ContextManager, tokenStatus: TokenStatus): string;
|
|
@@ -107,6 +107,11 @@ Extensions may register additional tools — follow their instructions.
|
|
|
107
107
|
- Keep bash commands focused; avoid long-running blocking commands
|
|
108
108
|
- Always check command exit codes for errors
|
|
109
109
|
|
|
110
|
+
# Context Envelopes
|
|
111
|
+
- \`<query_context>\` (e.g. \`<shell_events>\`): the user's situation when they sent this turn — ground "fix this" / "what just happened" requests with it.
|
|
112
|
+
- \`<dynamic_context>\`: current system state — in-flight work, mode markers, warnings.
|
|
113
|
+
Either may be absent on any turn.
|
|
114
|
+
|
|
110
115
|
# Preference Learning
|
|
111
116
|
|
|
112
117
|
Treat the user's past commands as standing preferences. Before acting, check shell history
|
|
@@ -130,27 +135,3 @@ export function buildStaticByCwd(cwd) {
|
|
|
130
135
|
}
|
|
131
136
|
return sections.join("\n\n");
|
|
132
137
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Per-iteration dynamic context: date, working directory, token usage.
|
|
135
|
-
* Rebuilt every LLM call. Extension advisors add more sections (budget,
|
|
136
|
-
* subagents, metacognitive signals, etc.) on top.
|
|
137
|
-
*
|
|
138
|
-
* Skills, AGENTS.md, and project conventions live in the system prompt
|
|
139
|
-
* (see `system-prompt:build` in agent-loop) so they enter the provider's
|
|
140
|
-
* prefix cache instead of being rebuilt and re-sent every turn.
|
|
141
|
-
*
|
|
142
|
-
* Shell context is likewise not injected here — it flows into the
|
|
143
|
-
* conversation as incremental <shell-events> messages (see
|
|
144
|
-
* AgentLoop.injectShellDelta) for the same reason.
|
|
145
|
-
*/
|
|
146
|
-
export function buildDynamicContext(contextManager, tokenStatus) {
|
|
147
|
-
const envLines = [
|
|
148
|
-
`Current date: ${new Date().toISOString().split("T")[0]}`,
|
|
149
|
-
`Working directory: ${contextManager.getCwd()}`,
|
|
150
|
-
];
|
|
151
|
-
const usedK = (tokenStatus.promptTokens / 1000).toFixed(1);
|
|
152
|
-
const maxK = (tokenStatus.contextWindow / 1000).toFixed(0);
|
|
153
|
-
const pct = Math.min(100, Math.round((tokenStatus.promptTokens / tokenStatus.contextWindow) * 100));
|
|
154
|
-
envLines.push(`Token usage: ${usedK}k/${maxK}k (${pct}%)`);
|
|
155
|
-
return `<environment>\n${envLines.join("\n")}\n</environment>`;
|
|
156
|
-
}
|
package/dist/core.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core kernel — the minimum viable agent-sh.
|
|
3
3
|
*
|
|
4
|
-
* Wires up EventBus +
|
|
4
|
+
* Wires up EventBus + HandlerRegistry without any frontend or agent backend.
|
|
5
5
|
* Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
|
|
6
|
-
* subscribing to bus events.
|
|
6
|
+
* subscribing to bus events. Shell-specific tracking lives in the
|
|
7
|
+
* shell-context built-in extension.
|
|
7
8
|
*
|
|
8
9
|
* Agent backends are loaded as extensions and register themselves via
|
|
9
10
|
* the agent:register-backend bus event. The built-in "ash" backend is
|
|
@@ -17,7 +18,6 @@
|
|
|
17
18
|
* const response = await core.query("hello");
|
|
18
19
|
*/
|
|
19
20
|
import { EventBus } from "./event-bus.js";
|
|
20
|
-
import { ContextManager } from "./context-manager.js";
|
|
21
21
|
import type { AgentShellConfig, ExtensionContext } from "./types.js";
|
|
22
22
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
23
23
|
export { EventBus } from "./event-bus.js";
|
|
@@ -32,7 +32,6 @@ export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "
|
|
|
32
32
|
export type { NuclearEntry } from "./agent/nuclear-form.js";
|
|
33
33
|
export interface AgentShellCore {
|
|
34
34
|
bus: EventBus;
|
|
35
|
-
contextManager: ContextManager;
|
|
36
35
|
/** Handler registry for define/advise/call. */
|
|
37
36
|
handlers: HandlerRegistry;
|
|
38
37
|
/** Unique id for this agent process; used for shell-marker tagging and lineage tracking. */
|
package/dist/core.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core kernel — the minimum viable agent-sh.
|
|
3
3
|
*
|
|
4
|
-
* Wires up EventBus +
|
|
4
|
+
* Wires up EventBus + HandlerRegistry without any frontend or agent backend.
|
|
5
5
|
* Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
|
|
6
|
-
* subscribing to bus events.
|
|
6
|
+
* subscribing to bus events. Shell-specific tracking lives in the
|
|
7
|
+
* shell-context built-in extension.
|
|
7
8
|
*
|
|
8
9
|
* Agent backends are loaded as extensions and register themselves via
|
|
9
10
|
* the agent:register-backend bus event. The built-in "ash" backend is
|
|
@@ -17,17 +18,15 @@
|
|
|
17
18
|
* const response = await core.query("hello");
|
|
18
19
|
*/
|
|
19
20
|
import { EventBus } from "./event-bus.js";
|
|
20
|
-
import { ContextManager } from "./context-manager.js";
|
|
21
21
|
import { createLlmFacade } from "./utils/llm-facade.js";
|
|
22
22
|
import { setPalette } from "./utils/palette.js";
|
|
23
23
|
import * as streamTransform from "./utils/stream-transform.js";
|
|
24
24
|
import * as settingsMod from "./settings.js";
|
|
25
25
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
26
|
-
import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
27
26
|
import crypto from "node:crypto";
|
|
28
27
|
import * as fs from "node:fs";
|
|
29
28
|
import * as path from "node:path";
|
|
30
|
-
import { DefaultCompositor
|
|
29
|
+
import { DefaultCompositor } from "./utils/compositor.js";
|
|
31
30
|
import { CONFIG_DIR } from "./settings.js";
|
|
32
31
|
// Re-export types that library consumers need
|
|
33
32
|
export { EventBus } from "./event-bus.js";
|
|
@@ -38,7 +37,6 @@ export { HistoryFile, InMemoryHistory, NoopHistory } from "./agent/history-file.
|
|
|
38
37
|
export function createCore(config) {
|
|
39
38
|
const bus = new EventBus();
|
|
40
39
|
const handlers = new HandlerRegistry();
|
|
41
|
-
const contextManager = new ContextManager(bus, handlers);
|
|
42
40
|
// 3 bytes = 6 hex chars, ~16M values — ample for per-lineage uniqueness and
|
|
43
41
|
// short enough to read/remember. Legacy content may have 16-char iids; any
|
|
44
42
|
// parsers should accept ≥6 hex chars.
|
|
@@ -48,6 +46,14 @@ export function createCore(config) {
|
|
|
48
46
|
// Expose raw CLI config so the agent backend extension can resolve
|
|
49
47
|
// providers and create the LLM client.
|
|
50
48
|
handlers.define("config:get-shell-config", () => config);
|
|
49
|
+
// Default; shell-context advises with the PTY-tracked cwd when loaded.
|
|
50
|
+
handlers.define("cwd", () => process.cwd());
|
|
51
|
+
// Empty defaults so registerContextProducer can advise regardless of
|
|
52
|
+
// backend. Each backend chooses whether to consume the strings — ash
|
|
53
|
+
// wraps them in <dynamic_context>/<query_context>; bridges may pull
|
|
54
|
+
// query-context:build and splice into the target SDK however they like.
|
|
55
|
+
handlers.define("dynamic-context:build", () => "");
|
|
56
|
+
handlers.define("query-context:build", () => "");
|
|
51
57
|
const backends = new Map();
|
|
52
58
|
let activeBackendName = null;
|
|
53
59
|
const activateByName = async (name, silent = false) => {
|
|
@@ -91,22 +97,12 @@ export function createCore(config) {
|
|
|
91
97
|
return { names, active: activeBackendName };
|
|
92
98
|
});
|
|
93
99
|
// ── Compositor ──────────────────────────────────────────────
|
|
100
|
+
// Generic surface-routing primitive. No defaults here — the active
|
|
101
|
+
// frontend (src/shell/, a web bridge, headless test harness, etc.)
|
|
102
|
+
// sets its own surfaces during activation.
|
|
94
103
|
const compositor = new DefaultCompositor(bus);
|
|
95
|
-
const stdoutSurface = new StdoutSurface();
|
|
96
|
-
compositor.setDefault("agent", stdoutSurface);
|
|
97
|
-
compositor.setDefault("query", stdoutSurface);
|
|
98
|
-
compositor.setDefault("status", stdoutSurface);
|
|
99
|
-
// ── Lazy singleton terminal buffer ──────────────────────────
|
|
100
|
-
let terminalBufferSingleton; // undefined = not yet created
|
|
101
|
-
const getTerminalBuffer = () => {
|
|
102
|
-
if (terminalBufferSingleton !== undefined)
|
|
103
|
-
return terminalBufferSingleton;
|
|
104
|
-
terminalBufferSingleton = TerminalBuffer.createWired(bus);
|
|
105
|
-
return terminalBufferSingleton;
|
|
106
|
-
};
|
|
107
104
|
return {
|
|
108
105
|
bus,
|
|
109
|
-
contextManager,
|
|
110
106
|
handlers,
|
|
111
107
|
instanceId,
|
|
112
108
|
activateBackend() {
|
|
@@ -162,7 +158,6 @@ export function createCore(config) {
|
|
|
162
158
|
extensionContext(opts) {
|
|
163
159
|
const ctx = {
|
|
164
160
|
bus,
|
|
165
|
-
contextManager,
|
|
166
161
|
instanceId,
|
|
167
162
|
llm: createLlmFacade(handlers),
|
|
168
163
|
providers: {
|
|
@@ -186,11 +181,25 @@ export function createCore(config) {
|
|
|
186
181
|
removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
|
|
187
182
|
registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
|
|
188
183
|
removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
|
|
184
|
+
registerContextProducer: (_name, producer, opts) => {
|
|
185
|
+
const handlerName = opts?.mode === "per-query"
|
|
186
|
+
? "query-context:build"
|
|
187
|
+
: "dynamic-context:build";
|
|
188
|
+
return handlers.advise(handlerName, (next) => {
|
|
189
|
+
const base = next();
|
|
190
|
+
const part = producer();
|
|
191
|
+
if (!part)
|
|
192
|
+
return base;
|
|
193
|
+
const trimmed = part.trim();
|
|
194
|
+
if (!trimmed)
|
|
195
|
+
return base;
|
|
196
|
+
return base ? `${base}\n\n${trimmed}` : trimmed;
|
|
197
|
+
});
|
|
198
|
+
},
|
|
189
199
|
define: (name, fn) => handlers.define(name, fn),
|
|
190
200
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
191
201
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
192
202
|
list: () => handlers.list(),
|
|
193
|
-
get terminalBuffer() { return getTerminalBuffer(); },
|
|
194
203
|
compositor,
|
|
195
204
|
onDispose: () => { },
|
|
196
205
|
createRemoteSession: (opts) => {
|
|
@@ -214,12 +223,6 @@ export function createCore(config) {
|
|
|
214
223
|
if (opts.suppressUsage !== false) {
|
|
215
224
|
cleanups.push(handlers.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
|
|
216
225
|
}
|
|
217
|
-
if (opts.interactive) {
|
|
218
|
-
cleanups.push(handlers.advise("dynamic-context:build", (next) => {
|
|
219
|
-
const base = next();
|
|
220
|
-
return active ? base + "\ninteractive-session: true\n" : base;
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
226
|
return {
|
|
224
227
|
submit(query) { bus.emit("agent:submit", { query }); },
|
|
225
228
|
get surface() { return surface; },
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
/** Resolve a usable bash binary, or null if none is on PATH.
|
|
3
|
+
* Unix: `/bin/bash` (canonical, present on every Linux/macOS install).
|
|
4
|
+
* Windows: probe via `where bash` so Git Bash users keep working. */
|
|
5
|
+
export declare function findBash(): string | null;
|
|
2
6
|
export interface ExecutorSession {
|
|
3
7
|
id: string;
|
|
4
8
|
command: string;
|
package/dist/executor.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { stripAnsi } from "./utils/ansi.js";
|
|
3
|
+
let cachedBashPath;
|
|
4
|
+
/** Resolve a usable bash binary, or null if none is on PATH.
|
|
5
|
+
* Unix: `/bin/bash` (canonical, present on every Linux/macOS install).
|
|
6
|
+
* Windows: probe via `where bash` so Git Bash users keep working. */
|
|
7
|
+
export function findBash() {
|
|
8
|
+
if (cachedBashPath !== undefined)
|
|
9
|
+
return cachedBashPath;
|
|
10
|
+
if (process.platform !== "win32") {
|
|
11
|
+
cachedBashPath = "/bin/bash";
|
|
12
|
+
return cachedBashPath;
|
|
13
|
+
}
|
|
14
|
+
const r = spawnSync("where", ["bash"], { encoding: "utf-8" });
|
|
15
|
+
cachedBashPath = r.status === 0 ? r.stdout.split(/\r?\n/)[0].trim() || null : null;
|
|
16
|
+
return cachedBashPath;
|
|
17
|
+
}
|
|
3
18
|
const DEFAULT_TIMEOUT = 60_000;
|
|
4
19
|
const DEFAULT_MAX_OUTPUT = 256 * 1024; // 256KB
|
|
5
20
|
/**
|
|
@@ -29,9 +44,12 @@ export function executeCommand(opts) {
|
|
|
29
44
|
if (v !== undefined)
|
|
30
45
|
env[k] = v;
|
|
31
46
|
}
|
|
47
|
+
const bashPath = findBash();
|
|
32
48
|
let child;
|
|
33
49
|
try {
|
|
34
|
-
|
|
50
|
+
if (!bashPath)
|
|
51
|
+
throw new Error("bash not found on PATH");
|
|
52
|
+
child = spawn(bashPath, ["-c", opts.command], {
|
|
35
53
|
stdio: ["ignore", "pipe", "pipe"],
|
|
36
54
|
cwd: opts.cwd,
|
|
37
55
|
env,
|
package/dist/extension-loader.js
CHANGED
|
@@ -73,6 +73,12 @@ function createScopedContext(ctx, extensionName) {
|
|
|
73
73
|
bus.emit("agent:register-skill", { name, description, filePath, extensionName });
|
|
74
74
|
cleanups.push(() => bus.emit("agent:remove-skill", { name }));
|
|
75
75
|
};
|
|
76
|
+
// Track dynamic-context producer registrations
|
|
77
|
+
const scopedRegisterContextProducer = (name, producer) => {
|
|
78
|
+
const dispose = ctx.registerContextProducer(name, producer);
|
|
79
|
+
cleanups.push(dispose);
|
|
80
|
+
return dispose;
|
|
81
|
+
};
|
|
76
82
|
// Track tool registrations — extension name captured in scope
|
|
77
83
|
const scopedRegisterTool = (tool) => {
|
|
78
84
|
bus.emit("agent:register-tool", { tool, extensionName });
|
|
@@ -93,6 +99,7 @@ function createScopedContext(ctx, extensionName) {
|
|
|
93
99
|
removeInstruction: ctx.removeInstruction,
|
|
94
100
|
registerSkill: scopedRegisterSkill,
|
|
95
101
|
removeSkill: ctx.removeSkill,
|
|
102
|
+
registerContextProducer: scopedRegisterContextProducer,
|
|
96
103
|
registerTool: scopedRegisterTool,
|
|
97
104
|
unregisterTool: ctx.unregisterTool,
|
|
98
105
|
registerCommand: scopedRegisterCommand,
|
|
@@ -67,7 +67,6 @@ export default function agentBackend(ctx) {
|
|
|
67
67
|
// would hit a no-op stub.
|
|
68
68
|
const agentLoop = new AgentLoop({
|
|
69
69
|
bus,
|
|
70
|
-
contextManager: ctx.contextManager,
|
|
71
70
|
llmClient,
|
|
72
71
|
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
73
72
|
modes,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../types.js";
|
|
2
|
-
export default function activate(
|
|
2
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
export default function activate(
|
|
11
|
-
bus.onPipe("autocomplete:request", (payload) => {
|
|
10
|
+
export default function activate(ctx) {
|
|
11
|
+
ctx.bus.onPipe("autocomplete:request", (payload) => {
|
|
12
12
|
const atPos = payload.buffer.lastIndexOf("@");
|
|
13
13
|
if (atPos < 0 || (atPos > 0 && payload.buffer[atPos - 1] !== " ")) {
|
|
14
14
|
return payload;
|
|
@@ -17,7 +17,7 @@ export default function activate({ bus, contextManager }) {
|
|
|
17
17
|
if (afterAt.includes(" ") || !/^[a-zA-Z0-9_.\/-]*$/.test(afterAt)) {
|
|
18
18
|
return payload;
|
|
19
19
|
}
|
|
20
|
-
const files = listFiles(afterAt,
|
|
20
|
+
const files = listFiles(afterAt, ctx.call("cwd"));
|
|
21
21
|
if (files.length === 0)
|
|
22
22
|
return payload;
|
|
23
23
|
return { ...payload, items: [...payload.items, ...files] };
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* These extensions ship with agent-sh and load before user extensions.
|
|
5
5
|
* They receive unscoped contexts (not reloadable) and can be individually
|
|
6
6
|
* disabled via the `disabledBuiltins` setting in ~/.agent-sh/settings.json.
|
|
7
|
+
*
|
|
8
|
+
* For order-critical frontend bootstrap (the PTY shell), see `src/shell/`.
|
|
9
|
+
* That module exposes its own `activate(ctx, opts)` entry point, loaded
|
|
10
|
+
* specially from `src/index.ts` rather than through this manifest.
|
|
7
11
|
*/
|
|
8
12
|
import type { ExtensionContext } from "../types.js";
|
|
9
13
|
type ActivateFn = (ctx: ExtensionContext) => void;
|
package/dist/extensions/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const BUILTIN_EXTENSIONS = [
|
|
2
|
+
{ name: "shell-context", load: () => import("./shell-context.js").then(m => m.default) },
|
|
2
3
|
{ name: "agent-backend", load: () => import("./agent-backend.js").then(m => m.default) },
|
|
3
4
|
{ name: "openrouter",
|
|
4
5
|
when: () => !!process.env.OPENROUTER_API_KEY,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks PTY commands and cwd, spills long outputs, contributes the
|
|
3
|
+
* `<shell_events>` per-query envelope. Frontends without a PTY skip this
|
|
4
|
+
* built-in and the agent runs cwd-aware via core's process.cwd() default.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionContext } from "../types.js";
|
|
7
|
+
export default function activate(ctx: ExtensionContext): void;
|