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.
@@ -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;
@@ -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, buildDynamicContext, buildStaticByCwd, formatSkillsBlock, loadGlobalAgentsMd } from "./system-prompt.js";
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.contextManager.getCwd();
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.contextManager.getCwd());
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
- // Extensions compose additional context (git info, project rules, etc.)
797
- h.define("dynamic-context:build", () => {
798
- const contextWindow = this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
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
- // Prepend any shell events that preceded this query into the same
1036
- // user message, so the conversation reads chronologically and we
1037
- // don't emit two consecutive user-role messages (some providers
1038
- // reject that).
1039
- const preDelta = this.contextManager.getEventsSince(this.lastShellSeq);
1040
- const userContent = preDelta ? `${preDelta.text}\n\n${query}` : query;
1041
- if (preDelta)
1042
- this.lastShellSeq = preDelta.lastSeq;
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.contextManager.getCwd();
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.contextManager.getCwd();
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
- { role: "user", content: `<context>\n${dynamicContext}\n</context>` },
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 = {
@@ -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: [...messages, ...conversation.getMessages()],
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
- }
@@ -1,7 +1,30 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { executeArgv, killSession } from "../../executor.js";
2
- // Targets PowerShell 7+ (`pwsh`). Legacy `powershell.exe` is intentionally
3
- // not auto-fallback its tool surface diverges enough that compatibility
4
- // shims aren't worth the maintenance.
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: "pwsh",
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: "PowerShell (pwsh) not found on PATH. Install PowerShell 7: winget install Microsoft.PowerShell.",
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 + ContextManager without any frontend or agent backend.
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 + ContextManager without any frontend or agent backend.
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, StdoutSurface } from "./utils/compositor.js";
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: true,
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 spawn,
218
- // and Windows where negative pids aren't supported).
219
- try {
220
- process.kill(-proc.pid, "SIGTERM");
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
- try {
231
- process.kill(-proc.pid, "SIGKILL");
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
  }
@@ -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({ bus, contextManager }: ExtensionContext): void;
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({ bus, contextManager }) {
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, contextManager.getCwd());
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;
@@ -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;