agent-sh 0.12.13 → 0.12.15
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 +23 -38
- 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/agent/tools/pwsh.js +36 -5
- package/dist/core.d.ts +3 -4
- package/dist/core.js +30 -27
- package/dist/executor.js +14 -9
- 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,10 +7,11 @@ 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
|
|
@@ -89,20 +90,14 @@ export class AgentLoop {
|
|
|
89
90
|
lastErrorByFile = new Map(); // file path → error summary
|
|
90
91
|
static THINKING_LEVELS = ["off", "low", "medium", "high"];
|
|
91
92
|
bus;
|
|
92
|
-
contextManager;
|
|
93
93
|
llmClient;
|
|
94
94
|
handlers;
|
|
95
95
|
thinkingLevel = "off";
|
|
96
96
|
compositor = null;
|
|
97
97
|
toolProtocol;
|
|
98
98
|
instanceId;
|
|
99
|
-
// Cursor into ContextManager's exchange stream. Events with id > this
|
|
100
|
-
// have not yet been shown to the LLM. We inject the delta as a user
|
|
101
|
-
// message before each stream so the prefix stays cacheable.
|
|
102
|
-
lastShellSeq = 0;
|
|
103
99
|
constructor(config) {
|
|
104
100
|
this.bus = config.bus;
|
|
105
|
-
this.contextManager = config.contextManager;
|
|
106
101
|
this.llmClient = config.llmClient;
|
|
107
102
|
this.handlers = config.handlers;
|
|
108
103
|
this.compositor = config.compositor ?? null;
|
|
@@ -598,7 +593,7 @@ export class AgentLoop {
|
|
|
598
593
|
return `${raw}${context}`;
|
|
599
594
|
}
|
|
600
595
|
registerCoreTools() {
|
|
601
|
-
const getCwd = () => this.
|
|
596
|
+
const getCwd = () => this.handlers.call("cwd");
|
|
602
597
|
const getEnv = () => {
|
|
603
598
|
const env = {};
|
|
604
599
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -717,7 +712,7 @@ export class AgentLoop {
|
|
|
717
712
|
// Placed here so they enter the provider's prompt cache with the
|
|
718
713
|
// system prompt, and only re-materialize when cwd changes invalidate
|
|
719
714
|
// cachedSystemPrompt in executeLoop.
|
|
720
|
-
const projectStatic = buildStaticByCwd(this.
|
|
715
|
+
const projectStatic = buildStaticByCwd(this.handlers.call("cwd"));
|
|
721
716
|
if (projectStatic)
|
|
722
717
|
parts.push(projectStatic);
|
|
723
718
|
// Extension sections (tools, skills, instructions grouped by extension)
|
|
@@ -793,12 +788,9 @@ export class AgentLoop {
|
|
|
793
788
|
};
|
|
794
789
|
});
|
|
795
790
|
h.define("agent:get-self", () => this);
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const promptTokens = this.conversation.estimatePromptTokens();
|
|
800
|
-
return buildDynamicContext(this.contextManager, { promptTokens, contextWindow });
|
|
801
|
-
});
|
|
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.
|
|
802
794
|
// Full control over what the LLM sees: takes messages[], returns messages[].
|
|
803
795
|
// Default: pass through. Extensions can advise to compact, summarize,
|
|
804
796
|
// filter, reorder, inject — whatever strategy fits.
|
|
@@ -1032,14 +1024,14 @@ export class AgentLoop {
|
|
|
1032
1024
|
this.bus.emit("agent:processing-start", {});
|
|
1033
1025
|
let responseText = "";
|
|
1034
1026
|
try {
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
const
|
|
1040
|
-
const userContent =
|
|
1041
|
-
|
|
1042
|
-
|
|
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;
|
|
1043
1035
|
this.conversation.addUserMessage(userContent);
|
|
1044
1036
|
this.bus.emit("conversation:message-appended", { role: "user", content: query });
|
|
1045
1037
|
responseText = await this.executeLoop(signal);
|
|
@@ -1083,7 +1075,7 @@ export class AgentLoop {
|
|
|
1083
1075
|
// so live signals (budget, in-flight subagents, metacognitive warnings)
|
|
1084
1076
|
// are fresh.
|
|
1085
1077
|
let cachedSystemPrompt;
|
|
1086
|
-
let lastCwd = this.
|
|
1078
|
+
let lastCwd = this.handlers.call("cwd");
|
|
1087
1079
|
while (!signal.aborted) {
|
|
1088
1080
|
// Auto-compact when total context approaches the window limit.
|
|
1089
1081
|
const totalEstimate = this.conversation.estimatePromptTokens();
|
|
@@ -1104,7 +1096,7 @@ export class AgentLoop {
|
|
|
1104
1096
|
}
|
|
1105
1097
|
cachedSystemPrompt = undefined;
|
|
1106
1098
|
}
|
|
1107
|
-
const currentCwd = this.
|
|
1099
|
+
const currentCwd = this.handlers.call("cwd");
|
|
1108
1100
|
if (currentCwd !== lastCwd) {
|
|
1109
1101
|
cachedSystemPrompt = undefined;
|
|
1110
1102
|
lastCwd = currentCwd;
|
|
@@ -1519,24 +1511,17 @@ export class AgentLoop {
|
|
|
1519
1511
|
let reasoning = "";
|
|
1520
1512
|
const reasoningDetailsByIndex = new Map();
|
|
1521
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.
|
|
1522
1519
|
const rawMessages = [
|
|
1523
1520
|
{ role: "system", content: systemPrompt },
|
|
1524
|
-
|
|
1525
|
-
{ role: "assistant", content: "Understood." },
|
|
1526
|
-
...this.conversation.getMessages(),
|
|
1521
|
+
...wrapTrailingWithDynamicContext(this.conversation.getMessages(), dynamicContext, toolPrompt),
|
|
1527
1522
|
];
|
|
1528
1523
|
// Let extensions transform the message array (compact, summarize, filter, etc.)
|
|
1529
1524
|
const messages = this.handlers.call("conversation:prepare", rawMessages);
|
|
1530
|
-
// Tool protocol controls what goes in the API tools param vs dynamic context
|
|
1531
|
-
const apiTools = this.toolProtocol.getApiTools(this.toolRegistry.all());
|
|
1532
|
-
const toolPrompt = this.toolProtocol.getToolPrompt(this.toolRegistry.all());
|
|
1533
|
-
// Append tool catalog to dynamic context (closer to user query = better followed)
|
|
1534
|
-
if (toolPrompt) {
|
|
1535
|
-
const ctxMsg = messages[1]; // dynamic context user message
|
|
1536
|
-
if (ctxMsg && typeof ctxMsg.content === "string") {
|
|
1537
|
-
ctxMsg.content += "\n" + toolPrompt;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
1525
|
// Stream filter strips tool tags from display (inline mode only)
|
|
1541
1526
|
const streamFilter = this.toolProtocol.createStreamFilter(this.toolRegistry.all().map((t) => t.name));
|
|
1542
1527
|
const requestParams = {
|
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/agent/tools/pwsh.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { executeArgv, killSession } from "../../executor.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
let cachedPwshPath;
|
|
4
|
+
/** Resolve a usable PowerShell binary, or null if none is on PATH.
|
|
5
|
+
* Prefers PowerShell 7+ (`pwsh`), falls back to Windows PowerShell (`powershell`). */
|
|
6
|
+
function findPwsh() {
|
|
7
|
+
if (cachedPwshPath !== undefined)
|
|
8
|
+
return cachedPwshPath;
|
|
9
|
+
// Prefer PowerShell 7 (pwsh)
|
|
10
|
+
const pwsh = spawnSync("where", ["pwsh"], { encoding: "utf-8" });
|
|
11
|
+
if (pwsh.status === 0) {
|
|
12
|
+
cachedPwshPath = pwsh.stdout.split(/\r?\n/)[0].trim() || null;
|
|
13
|
+
if (cachedPwshPath)
|
|
14
|
+
return cachedPwshPath;
|
|
15
|
+
}
|
|
16
|
+
// Fallback to Windows PowerShell (powershell.exe)
|
|
17
|
+
const ps = spawnSync("where", ["powershell"], { encoding: "utf-8" });
|
|
18
|
+
cachedPwshPath = ps.status === 0 ? ps.stdout.split(/\r?\n/)[0].trim() || null : null;
|
|
19
|
+
return cachedPwshPath;
|
|
20
|
+
}
|
|
21
|
+
/** Return the PowerShell executable name for display purposes. */
|
|
22
|
+
function getPwshDisplayName() {
|
|
23
|
+
const path = findPwsh();
|
|
24
|
+
if (!path)
|
|
25
|
+
return "PowerShell";
|
|
26
|
+
return path.toLowerCase().includes("pwsh") ? "pwsh" : "powershell";
|
|
27
|
+
}
|
|
5
28
|
export function createPwshTool(opts) {
|
|
6
29
|
return {
|
|
7
30
|
name: "pwsh",
|
|
@@ -54,8 +77,16 @@ export function createPwshTool(opts) {
|
|
|
54
77
|
isError: false,
|
|
55
78
|
};
|
|
56
79
|
}
|
|
80
|
+
const pwshPath = findPwsh();
|
|
81
|
+
if (!pwshPath) {
|
|
82
|
+
return {
|
|
83
|
+
content: "PowerShell not found on PATH. Neither pwsh (PowerShell 7+) nor powershell (Windows PowerShell) is available.",
|
|
84
|
+
exitCode: 1,
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
57
88
|
const { session, done } = executeArgv({
|
|
58
|
-
file:
|
|
89
|
+
file: pwshPath,
|
|
59
90
|
args: ["-NoProfile", "-NonInteractive", "-Command", command],
|
|
60
91
|
cwd: opts.getCwd(),
|
|
61
92
|
env: opts.getEnv(),
|
|
@@ -72,7 +103,7 @@ export function createPwshTool(opts) {
|
|
|
72
103
|
}
|
|
73
104
|
if (session.spawnFailed) {
|
|
74
105
|
return {
|
|
75
|
-
content:
|
|
106
|
+
content: `${getPwshDisplayName()} not found on PATH. Install PowerShell 7: winget install Microsoft.PowerShell.`,
|
|
76
107
|
exitCode: 1,
|
|
77
108
|
isError: true,
|
|
78
109
|
};
|
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.js
CHANGED
|
@@ -53,7 +53,8 @@ export function executeCommand(opts) {
|
|
|
53
53
|
stdio: ["ignore", "pipe", "pipe"],
|
|
54
54
|
cwd: opts.cwd,
|
|
55
55
|
env,
|
|
56
|
-
detached:
|
|
56
|
+
detached: process.platform !== "win32",
|
|
57
|
+
windowsHide: true,
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
catch (err) {
|
|
@@ -214,12 +215,14 @@ export function killSession(session) {
|
|
|
214
215
|
if (!proc || !proc.pid)
|
|
215
216
|
return () => { };
|
|
216
217
|
// Try process-group kill first (works for executeCommand's detached bash
|
|
217
|
-
// children); fall back to direct kill (executeArgv's non-detached
|
|
218
|
-
// and Windows where negative pids aren't supported).
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
// children on Unix); fall back to direct kill (executeArgv's non-detached
|
|
219
|
+
// spawn, and Windows where negative pids aren't supported).
|
|
220
|
+
if (process.platform !== "win32") {
|
|
221
|
+
try {
|
|
222
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
221
225
|
}
|
|
222
|
-
catch { }
|
|
223
226
|
try {
|
|
224
227
|
proc.kill("SIGTERM");
|
|
225
228
|
}
|
|
@@ -227,10 +230,12 @@ export function killSession(session) {
|
|
|
227
230
|
let settled = false;
|
|
228
231
|
const fallback = setTimeout(() => {
|
|
229
232
|
if (!settled && !session.done && proc.pid) {
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
if (process.platform !== "win32") {
|
|
234
|
+
try {
|
|
235
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
232
238
|
}
|
|
233
|
-
catch { }
|
|
234
239
|
try {
|
|
235
240
|
proc.kill("SIGKILL");
|
|
236
241
|
}
|
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;
|