agent-sh 0.12.8 → 0.12.10
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 +4 -3
- package/dist/agent/agent-loop.js +29 -14
- package/dist/agent/history-file.d.ts +30 -1
- package/dist/agent/history-file.js +40 -17
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/agent/nuclear-form.js +19 -0
- package/dist/agent/tools/glob.js +1 -1
- package/dist/agent/tools/grep.js +1 -1
- package/dist/agent/tools/pwsh.d.ts +7 -0
- package/dist/agent/tools/pwsh.js +90 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +4 -0
- package/dist/event-bus.d.ts +4 -0
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +21 -0
- package/dist/extensions/agent-backend.js +16 -0
- package/dist/extensions/openai.d.ts +5 -3
- package/dist/extensions/openai.js +23 -20
- package/dist/extensions/openrouter.js +6 -0
- package/dist/settings.d.ts +5 -0
- package/dist/settings.js +1 -0
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import type { ContextManager } from "../context-manager.js";
|
|
|
17
17
|
import type { LlmClient } from "../utils/llm-client.js";
|
|
18
18
|
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
19
19
|
import type { AgentBackend, ToolDefinition } from "./types.js";
|
|
20
|
+
import { type HistoryAdapter } from "./history-file.js";
|
|
20
21
|
import type { Compositor } from "../utils/compositor.js";
|
|
21
22
|
export interface AgentLoopConfig {
|
|
22
23
|
bus: EventBus;
|
|
@@ -28,11 +29,12 @@ export interface AgentLoopConfig {
|
|
|
28
29
|
compositor?: Compositor;
|
|
29
30
|
/** Instance ID from core — ensures history entries match the ID in prompts. */
|
|
30
31
|
instanceId?: string;
|
|
32
|
+
history?: HistoryAdapter;
|
|
31
33
|
}
|
|
32
34
|
export declare class AgentLoop implements AgentBackend {
|
|
33
35
|
private abortController;
|
|
34
36
|
private toolRegistry;
|
|
35
|
-
private
|
|
37
|
+
private history;
|
|
36
38
|
private conversation;
|
|
37
39
|
private fileReadCache;
|
|
38
40
|
private modes;
|
|
@@ -100,8 +102,7 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
100
102
|
buildExtensionSections(): string[];
|
|
101
103
|
kill(): void;
|
|
102
104
|
private cancel;
|
|
103
|
-
|
|
104
|
-
private shouldSendReasoningEffort;
|
|
105
|
+
private reasoningParams;
|
|
105
106
|
private get currentMode();
|
|
106
107
|
private get currentModel();
|
|
107
108
|
/**
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -15,6 +15,7 @@ import { getSettings, updateSettings } from "../settings.js";
|
|
|
15
15
|
import { createToolProtocol } from "./tool-protocol.js";
|
|
16
16
|
// Core tool factories
|
|
17
17
|
import { createBashTool } from "./tools/bash.js";
|
|
18
|
+
import { createPwshTool } from "./tools/pwsh.js";
|
|
18
19
|
import { createReadFileTool } from "./tools/read-file.js";
|
|
19
20
|
import { createWriteFileTool } from "./tools/write-file.js";
|
|
20
21
|
import { createEditFileTool } from "./tools/edit-file.js";
|
|
@@ -49,7 +50,7 @@ function summarizeDescription(desc) {
|
|
|
49
50
|
export class AgentLoop {
|
|
50
51
|
abortController = null;
|
|
51
52
|
toolRegistry = new ToolRegistry();
|
|
52
|
-
|
|
53
|
+
history;
|
|
53
54
|
conversation;
|
|
54
55
|
fileReadCache = new Map();
|
|
55
56
|
modes;
|
|
@@ -109,7 +110,7 @@ export class AgentLoop {
|
|
|
109
110
|
// `history:append` handler registered below; extensions swap the
|
|
110
111
|
// backend without touching this wiring.
|
|
111
112
|
const filePath = process.env.AGENT_SH_HISTORY_FILE || getSettings().historyFilePath;
|
|
112
|
-
this.
|
|
113
|
+
this.history = config.history ?? new HistoryFile({ instanceId: this.instanceId, filePath });
|
|
113
114
|
this.conversation = new ConversationState(this.handlers, this.instanceId);
|
|
114
115
|
// Fall back to a single-mode placeholder if the caller passed an
|
|
115
116
|
// empty array (agent-backend does this pre-resolution).
|
|
@@ -179,6 +180,16 @@ export class AgentLoop {
|
|
|
179
180
|
message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
|
|
180
181
|
});
|
|
181
182
|
}
|
|
183
|
+
const active = this.modes[this.currentModeIndex];
|
|
184
|
+
if (active && active.contextWindow !== prev?.contextWindow) {
|
|
185
|
+
this.bus.emit("agent:info", {
|
|
186
|
+
name: "ash",
|
|
187
|
+
version: PACKAGE_VERSION,
|
|
188
|
+
model: active.model,
|
|
189
|
+
provider: active.provider,
|
|
190
|
+
contextWindow: active.contextWindow,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
182
193
|
this.bus.emit("config:changed", {});
|
|
183
194
|
});
|
|
184
195
|
// Fires before wire() too — agent-backend emits this from
|
|
@@ -460,16 +471,17 @@ export class AgentLoop {
|
|
|
460
471
|
cancel() {
|
|
461
472
|
this.abortController?.abort();
|
|
462
473
|
}
|
|
463
|
-
|
|
464
|
-
shouldSendReasoningEffort() {
|
|
465
|
-
if (this.thinkingLevel === "off")
|
|
466
|
-
return false;
|
|
474
|
+
reasoningParams() {
|
|
467
475
|
const mode = this.currentMode;
|
|
468
476
|
if (mode.reasoning === false)
|
|
469
|
-
return
|
|
477
|
+
return {};
|
|
470
478
|
if (mode.supportsReasoningEffort === false)
|
|
471
|
-
return
|
|
472
|
-
|
|
479
|
+
return {};
|
|
480
|
+
if (mode.buildReasoningParams)
|
|
481
|
+
return mode.buildReasoningParams(this.thinkingLevel);
|
|
482
|
+
if (this.thinkingLevel === "off")
|
|
483
|
+
return {};
|
|
484
|
+
return { reasoning_effort: this.thinkingLevel };
|
|
473
485
|
}
|
|
474
486
|
get currentMode() {
|
|
475
487
|
return this.modes[this.currentModeIndex];
|
|
@@ -591,6 +603,9 @@ export class AgentLoop {
|
|
|
591
603
|
return env;
|
|
592
604
|
};
|
|
593
605
|
this.toolRegistry.register(createBashTool({ getCwd, getEnv, bus: this.bus }));
|
|
606
|
+
if (process.platform === "win32") {
|
|
607
|
+
this.toolRegistry.register(createPwshTool({ getCwd, getEnv, bus: this.bus }));
|
|
608
|
+
}
|
|
594
609
|
this.toolRegistry.register(createReadFileTool(getCwd, this.fileReadCache));
|
|
595
610
|
this.toolRegistry.register(createWriteFileTool(getCwd));
|
|
596
611
|
this.toolRegistry.register(createEditFileTool(getCwd));
|
|
@@ -818,11 +833,11 @@ export class AgentLoop {
|
|
|
818
833
|
return;
|
|
819
834
|
const writable = entries.filter((e) => !isReadOnly(e));
|
|
820
835
|
if (writable.length > 0)
|
|
821
|
-
this.
|
|
836
|
+
this.history.append(writable).catch(() => { });
|
|
822
837
|
});
|
|
823
|
-
h.define("history:search", async (query) => this.
|
|
824
|
-
h.define("history:find-by-seq", async (seq) => this.
|
|
825
|
-
h.define("history:read-recent", async (max) => this.
|
|
838
|
+
h.define("history:search", async (query) => this.history.search(query));
|
|
839
|
+
h.define("history:find-by-seq", async (seq) => this.history.findBySeq(seq));
|
|
840
|
+
h.define("history:read-recent", async (max) => this.history.readRecent(max));
|
|
826
841
|
// Prior-session preamble renderer. Default: flat chronological list.
|
|
827
842
|
h.define("conversation:format-prior-history", (entries) => {
|
|
828
843
|
if (!entries || entries.length === 0)
|
|
@@ -1521,7 +1536,7 @@ export class AgentLoop {
|
|
|
1521
1536
|
messages,
|
|
1522
1537
|
tools: apiTools,
|
|
1523
1538
|
model: this.currentModel,
|
|
1524
|
-
|
|
1539
|
+
...this.reasoningParams(),
|
|
1525
1540
|
};
|
|
1526
1541
|
this.bus.emit("llm:request", requestParams);
|
|
1527
1542
|
const stream = await this.llmClient.stream({ ...requestParams, signal });
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { type NuclearEntry } from "./nuclear-form.js";
|
|
2
|
-
export
|
|
2
|
+
export interface HistoryAdapter {
|
|
3
|
+
append(entries: NuclearEntry[]): Promise<void>;
|
|
4
|
+
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
5
|
+
search(query: string): Promise<{
|
|
6
|
+
entry: NuclearEntry;
|
|
7
|
+
line: string;
|
|
8
|
+
}[]>;
|
|
9
|
+
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
10
|
+
}
|
|
11
|
+
export declare class InMemoryHistory implements HistoryAdapter {
|
|
12
|
+
private entries;
|
|
13
|
+
constructor(initial?: NuclearEntry[]);
|
|
14
|
+
append(entries: NuclearEntry[]): Promise<void>;
|
|
15
|
+
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
16
|
+
search(query: string): Promise<{
|
|
17
|
+
entry: NuclearEntry;
|
|
18
|
+
line: string;
|
|
19
|
+
}[]>;
|
|
20
|
+
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare class NoopHistory implements HistoryAdapter {
|
|
23
|
+
append(): Promise<void>;
|
|
24
|
+
readRecent(): Promise<NuclearEntry[]>;
|
|
25
|
+
search(): Promise<{
|
|
26
|
+
entry: NuclearEntry;
|
|
27
|
+
line: string;
|
|
28
|
+
}[]>;
|
|
29
|
+
findBySeq(): Promise<NuclearEntry | null>;
|
|
30
|
+
}
|
|
31
|
+
export declare class HistoryFile implements HistoryAdapter {
|
|
3
32
|
readonly instanceId: string;
|
|
4
33
|
private filePath;
|
|
5
34
|
private lockPath;
|
|
@@ -10,9 +10,43 @@ import * as fss from "node:fs";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import * as crypto from "node:crypto";
|
|
12
12
|
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
13
|
-
import { serializeEntry, deserializeEntry,
|
|
13
|
+
import { serializeEntry, deserializeEntry, isReadOnly, compileSearchRegex, matchEntry, } from "./nuclear-form.js";
|
|
14
14
|
const HISTORY_PATH = path.join(CONFIG_DIR, "history");
|
|
15
15
|
const LOCK_STALE_MS = 10_000; // consider lock stale after 10s
|
|
16
|
+
export class InMemoryHistory {
|
|
17
|
+
entries;
|
|
18
|
+
constructor(initial = []) {
|
|
19
|
+
this.entries = [...initial];
|
|
20
|
+
}
|
|
21
|
+
async append(entries) {
|
|
22
|
+
this.entries.push(...entries);
|
|
23
|
+
}
|
|
24
|
+
async readRecent(maxEntries) {
|
|
25
|
+
const filtered = this.entries.filter((e) => !isReadOnly(e));
|
|
26
|
+
return maxEntries ? filtered.slice(-maxEntries) : filtered;
|
|
27
|
+
}
|
|
28
|
+
async search(query) {
|
|
29
|
+
if (!query.trim())
|
|
30
|
+
return [];
|
|
31
|
+
const re = compileSearchRegex(query);
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
34
|
+
const m = matchEntry(this.entries[i], re);
|
|
35
|
+
if (m)
|
|
36
|
+
out.push(m);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
async findBySeq(seq) {
|
|
41
|
+
return this.entries.find((e) => e.seq === seq) ?? null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class NoopHistory {
|
|
45
|
+
async append() { }
|
|
46
|
+
async readRecent() { return []; }
|
|
47
|
+
async search() { return []; }
|
|
48
|
+
async findBySeq() { return null; }
|
|
49
|
+
}
|
|
16
50
|
export class HistoryFile {
|
|
17
51
|
instanceId;
|
|
18
52
|
filePath;
|
|
@@ -65,16 +99,7 @@ export class HistoryFile {
|
|
|
65
99
|
async search(query) {
|
|
66
100
|
if (!query.trim())
|
|
67
101
|
return [];
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
regex = new RegExp(query, "i");
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
74
|
-
const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
75
|
-
const lookaheads = escaped.map((w) => `(?=.*${w})`).join("");
|
|
76
|
-
regex = new RegExp(lookaheads, "i");
|
|
77
|
-
}
|
|
102
|
+
const regex = compileSearchRegex(query);
|
|
78
103
|
const budgetBytes = 20 * 1024 * 1024;
|
|
79
104
|
let scanned = 0;
|
|
80
105
|
const results = [];
|
|
@@ -83,13 +108,11 @@ export class HistoryFile {
|
|
|
83
108
|
if (scanned > budgetBytes)
|
|
84
109
|
break;
|
|
85
110
|
const entry = deserializeEntry(line);
|
|
86
|
-
if (!entry
|
|
111
|
+
if (!entry)
|
|
87
112
|
continue;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
results.push({ entry, line: formatNuclearLine(entry) });
|
|
92
|
-
}
|
|
113
|
+
const m = matchEntry(entry, regex);
|
|
114
|
+
if (m)
|
|
115
|
+
results.push(m);
|
|
93
116
|
}
|
|
94
117
|
return results;
|
|
95
118
|
}
|
|
@@ -66,3 +66,10 @@ export declare function serializeEntry(entry: NuclearEntry): string;
|
|
|
66
66
|
export declare function deserializeEntry(line: string): NuclearEntry | null;
|
|
67
67
|
/** Check if a nuclear entry represents a read-only action (should be dropped). */
|
|
68
68
|
export declare function isReadOnly(entry: NuclearEntry): boolean;
|
|
69
|
+
/** Compile a search query, falling back to whitespace-split AND-of-words on invalid regex. */
|
|
70
|
+
export declare function compileSearchRegex(query: string): RegExp;
|
|
71
|
+
/** Match a writable entry against a search regex; null if filtered or no match. */
|
|
72
|
+
export declare function matchEntry(entry: NuclearEntry, re: RegExp): {
|
|
73
|
+
entry: NuclearEntry;
|
|
74
|
+
line: string;
|
|
75
|
+
} | null;
|
|
@@ -200,6 +200,25 @@ export function isReadOnly(entry) {
|
|
|
200
200
|
return false;
|
|
201
201
|
return READ_ONLY_TOOLS.has(entry.tool) || extraReadOnlyTools.has(entry.tool);
|
|
202
202
|
}
|
|
203
|
+
/** Compile a search query, falling back to whitespace-split AND-of-words on invalid regex. */
|
|
204
|
+
export function compileSearchRegex(query) {
|
|
205
|
+
try {
|
|
206
|
+
return new RegExp(query, "i");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
210
|
+
const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
211
|
+
const lookaheads = escaped.map((w) => `(?=.*${w})`).join("");
|
|
212
|
+
return new RegExp(lookaheads, "i");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Match a writable entry against a search regex; null if filtered or no match. */
|
|
216
|
+
export function matchEntry(entry, re) {
|
|
217
|
+
if (isReadOnly(entry))
|
|
218
|
+
return null;
|
|
219
|
+
const text = [entry.sum, entry.body].filter(Boolean).join("\n");
|
|
220
|
+
return re.test(text) ? { entry, line: formatNuclearLine(entry) } : null;
|
|
221
|
+
}
|
|
203
222
|
// ── Internal helpers ──────────────────────────────────────────────
|
|
204
223
|
function truncate(text, maxLen) {
|
|
205
224
|
const oneLine = text.replace(/\n/g, " ").trim();
|
package/dist/agent/tools/glob.js
CHANGED
|
@@ -50,7 +50,7 @@ export function createGlobTool(getCwd) {
|
|
|
50
50
|
timeout: 10_000,
|
|
51
51
|
});
|
|
52
52
|
await done;
|
|
53
|
-
if (session.
|
|
53
|
+
if (session.spawnFailed) {
|
|
54
54
|
return {
|
|
55
55
|
content: "ripgrep not available — the bundled binary failed to load and `rg` is not on PATH. Reinstall agent-sh, or install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation).",
|
|
56
56
|
exitCode: 1,
|
package/dist/agent/tools/grep.js
CHANGED
|
@@ -127,7 +127,7 @@ export function createGrepTool(getCwd) {
|
|
|
127
127
|
maxOutputBytes: 64 * 1024,
|
|
128
128
|
});
|
|
129
129
|
await done;
|
|
130
|
-
if (session.
|
|
130
|
+
if (session.spawnFailed) {
|
|
131
131
|
return {
|
|
132
132
|
content: "ripgrep not available — the bundled binary failed to load and `rg` is not on PATH. Reinstall agent-sh, or install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation).",
|
|
133
133
|
exitCode: 1,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
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.
|
|
5
|
+
export function createPwshTool(opts) {
|
|
6
|
+
return {
|
|
7
|
+
name: "pwsh",
|
|
8
|
+
description: "Execute a PowerShell command in an isolated subprocess. " +
|
|
9
|
+
"Use this on Windows when the `bash` tool fails (no /bin/bash available). " +
|
|
10
|
+
"Use PowerShell syntax — e.g. `Get-ChildItem`, `Select-String`, `$env:HOME`. " +
|
|
11
|
+
"Does not affect the user's shell state. " +
|
|
12
|
+
"cwd is set to the working directory from the shell context. " +
|
|
13
|
+
"Do NOT use pwsh for file searching — use grep/glob instead. " +
|
|
14
|
+
"Do NOT use pwsh for reading files — use read_file instead.",
|
|
15
|
+
input_schema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
command: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "The PowerShell command to execute",
|
|
21
|
+
},
|
|
22
|
+
timeout: {
|
|
23
|
+
type: "number",
|
|
24
|
+
description: "Timeout in seconds (default: 60)",
|
|
25
|
+
},
|
|
26
|
+
description: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Short description of what this command does (e.g., 'Install dependencies', 'Run test suite')",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["command"],
|
|
32
|
+
},
|
|
33
|
+
showOutput: true,
|
|
34
|
+
modifiesFiles: true,
|
|
35
|
+
requiresPermission: true,
|
|
36
|
+
getDisplayInfo: () => ({
|
|
37
|
+
kind: "execute",
|
|
38
|
+
icon: "▶",
|
|
39
|
+
locations: [],
|
|
40
|
+
}),
|
|
41
|
+
async execute(args, onChunk, ctx) {
|
|
42
|
+
const command = args.command;
|
|
43
|
+
const timeout = (args.timeout ?? 60) * 1000;
|
|
44
|
+
const intercepted = opts.bus.emitPipe("agent:terminal-intercept", {
|
|
45
|
+
command,
|
|
46
|
+
cwd: opts.getCwd(),
|
|
47
|
+
intercepted: false,
|
|
48
|
+
output: "",
|
|
49
|
+
});
|
|
50
|
+
if (intercepted.intercepted) {
|
|
51
|
+
return {
|
|
52
|
+
content: intercepted.output,
|
|
53
|
+
exitCode: 0,
|
|
54
|
+
isError: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const { session, done } = executeArgv({
|
|
58
|
+
file: "pwsh",
|
|
59
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", command],
|
|
60
|
+
cwd: opts.getCwd(),
|
|
61
|
+
env: opts.getEnv(),
|
|
62
|
+
timeout,
|
|
63
|
+
onOutput: onChunk,
|
|
64
|
+
});
|
|
65
|
+
const onAbort = () => killSession(session);
|
|
66
|
+
ctx?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
67
|
+
try {
|
|
68
|
+
await done;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
ctx?.signal?.removeEventListener("abort", onAbort);
|
|
72
|
+
}
|
|
73
|
+
if (session.spawnFailed) {
|
|
74
|
+
return {
|
|
75
|
+
content: "PowerShell (pwsh) not found on PATH. Install PowerShell 7: winget install Microsoft.PowerShell.",
|
|
76
|
+
exitCode: 1,
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const content = session.truncated
|
|
81
|
+
? `[output truncated, showing last portion]\n${session.output}`
|
|
82
|
+
: session.output;
|
|
83
|
+
return {
|
|
84
|
+
content: content || "(no output)",
|
|
85
|
+
exitCode: session.exitCode,
|
|
86
|
+
isError: session.exitCode !== 0,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
package/dist/core.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type { ColorPalette } from "./utils/palette.js";
|
|
|
28
28
|
export type { AgentBackend, ToolDefinition } from "./agent/types.js";
|
|
29
29
|
export { runSubagent, type SubagentOptions } from "./agent/subagent.js";
|
|
30
30
|
export { LlmClient } from "./utils/llm-client.js";
|
|
31
|
+
export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "./agent/history-file.js";
|
|
32
|
+
export type { NuclearEntry } from "./agent/nuclear-form.js";
|
|
31
33
|
export interface AgentShellCore {
|
|
32
34
|
bus: EventBus;
|
|
33
35
|
contextManager: ContextManager;
|
package/dist/core.js
CHANGED
|
@@ -34,6 +34,7 @@ export { EventBus } from "./event-bus.js";
|
|
|
34
34
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
35
35
|
export { runSubagent } from "./agent/subagent.js";
|
|
36
36
|
export { LlmClient } from "./utils/llm-client.js";
|
|
37
|
+
export { HistoryFile, InMemoryHistory, NoopHistory } from "./agent/history-file.js";
|
|
37
38
|
export function createCore(config) {
|
|
38
39
|
const bus = new EventBus();
|
|
39
40
|
const handlers = new HandlerRegistry();
|
|
@@ -164,6 +165,9 @@ export function createCore(config) {
|
|
|
164
165
|
contextManager,
|
|
165
166
|
instanceId,
|
|
166
167
|
llm: createLlmFacade(handlers),
|
|
168
|
+
providers: {
|
|
169
|
+
configure: (id, opts) => bus.emit("provider:configure", { id, ...opts }),
|
|
170
|
+
},
|
|
167
171
|
quit: opts.quit,
|
|
168
172
|
setPalette,
|
|
169
173
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -320,6 +320,10 @@ export interface ShellEvents {
|
|
|
320
320
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
321
321
|
supportsReasoningEffort?: boolean;
|
|
322
322
|
};
|
|
323
|
+
"provider:configure": {
|
|
324
|
+
id: string;
|
|
325
|
+
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
326
|
+
};
|
|
323
327
|
"agent:register-tool": {
|
|
324
328
|
tool: import("./agent/types.js").ToolDefinition;
|
|
325
329
|
extensionName?: string;
|
package/dist/executor.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export interface ExecutorSession {
|
|
|
6
6
|
exitCode: number | null;
|
|
7
7
|
done: boolean;
|
|
8
8
|
truncated: boolean;
|
|
9
|
+
/** True when the binary couldn't be launched (ENOENT, EACCES). Lets callers
|
|
10
|
+
* distinguish "tool missing" from "tool ran and exited with -1". */
|
|
11
|
+
spawnFailed: boolean;
|
|
9
12
|
process: ChildProcess | null;
|
|
10
13
|
resolve?: () => void;
|
|
11
14
|
}
|
package/dist/executor.js
CHANGED
|
@@ -16,6 +16,7 @@ export function executeCommand(opts) {
|
|
|
16
16
|
exitCode: null,
|
|
17
17
|
done: false,
|
|
18
18
|
truncated: false,
|
|
19
|
+
spawnFailed: false,
|
|
19
20
|
process: null,
|
|
20
21
|
};
|
|
21
22
|
const done = new Promise((resolve) => {
|
|
@@ -39,6 +40,7 @@ export function executeCommand(opts) {
|
|
|
39
40
|
}
|
|
40
41
|
catch (err) {
|
|
41
42
|
session.exitCode = -1;
|
|
43
|
+
session.spawnFailed = true;
|
|
42
44
|
session.output = `Failed to spawn: ${err instanceof Error ? err.message : String(err)}`;
|
|
43
45
|
session.done = true;
|
|
44
46
|
session.resolve?.();
|
|
@@ -79,6 +81,9 @@ export function executeCommand(opts) {
|
|
|
79
81
|
cancelKill?.();
|
|
80
82
|
if (!session.done) {
|
|
81
83
|
session.exitCode = -1;
|
|
84
|
+
const code = err.code;
|
|
85
|
+
if (code === "ENOENT" || code === "EACCES")
|
|
86
|
+
session.spawnFailed = true;
|
|
82
87
|
session.output += `\nProcess error: ${err.message}`;
|
|
83
88
|
session.done = true;
|
|
84
89
|
session.process = null;
|
|
@@ -102,6 +107,7 @@ export function executeArgv(opts) {
|
|
|
102
107
|
exitCode: null,
|
|
103
108
|
done: false,
|
|
104
109
|
truncated: false,
|
|
110
|
+
spawnFailed: false,
|
|
105
111
|
process: null,
|
|
106
112
|
};
|
|
107
113
|
const done = new Promise((resolve) => {
|
|
@@ -123,6 +129,7 @@ export function executeArgv(opts) {
|
|
|
123
129
|
}
|
|
124
130
|
catch (err) {
|
|
125
131
|
session.exitCode = -1;
|
|
132
|
+
session.spawnFailed = true;
|
|
126
133
|
session.output = `Failed to spawn ${opts.file}: ${err instanceof Error ? err.message : String(err)}`;
|
|
127
134
|
session.done = true;
|
|
128
135
|
session.resolve?.();
|
|
@@ -168,6 +175,9 @@ export function executeArgv(opts) {
|
|
|
168
175
|
clearTimeout(timer);
|
|
169
176
|
if (!session.done) {
|
|
170
177
|
session.exitCode = -1;
|
|
178
|
+
const code = err.code;
|
|
179
|
+
if (code === "ENOENT" || code === "EACCES")
|
|
180
|
+
session.spawnFailed = true;
|
|
171
181
|
session.output += `\nProcess error: ${err.message}`;
|
|
172
182
|
session.done = true;
|
|
173
183
|
session.process = null;
|
|
@@ -185,10 +195,17 @@ export function killSession(session) {
|
|
|
185
195
|
const proc = session.process;
|
|
186
196
|
if (!proc || !proc.pid)
|
|
187
197
|
return () => { };
|
|
198
|
+
// Try process-group kill first (works for executeCommand's detached bash
|
|
199
|
+
// children); fall back to direct kill (executeArgv's non-detached spawn,
|
|
200
|
+
// and Windows where negative pids aren't supported).
|
|
188
201
|
try {
|
|
189
202
|
process.kill(-proc.pid, "SIGTERM");
|
|
190
203
|
}
|
|
191
204
|
catch { }
|
|
205
|
+
try {
|
|
206
|
+
proc.kill("SIGTERM");
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
192
209
|
let settled = false;
|
|
193
210
|
const fallback = setTimeout(() => {
|
|
194
211
|
if (!settled && !session.done && proc.pid) {
|
|
@@ -196,6 +213,10 @@ export function killSession(session) {
|
|
|
196
213
|
process.kill(-proc.pid, "SIGKILL");
|
|
197
214
|
}
|
|
198
215
|
catch { }
|
|
216
|
+
try {
|
|
217
|
+
proc.kill("SIGKILL");
|
|
218
|
+
}
|
|
219
|
+
catch { }
|
|
199
220
|
}
|
|
200
221
|
}, 5000);
|
|
201
222
|
fallback.unref();
|
|
@@ -8,6 +8,9 @@ function persistedModelFor(providerName) {
|
|
|
8
8
|
return undefined;
|
|
9
9
|
return getSettings().providers?.[providerName]?.defaultModel;
|
|
10
10
|
}
|
|
11
|
+
function defaultReasoningBuilder(level) {
|
|
12
|
+
return level === "off" ? {} : { reasoning_effort: level };
|
|
13
|
+
}
|
|
11
14
|
export default function agentBackend(ctx) {
|
|
12
15
|
const { bus } = ctx;
|
|
13
16
|
const config = ctx.call("config:get-shell-config") ?? {};
|
|
@@ -18,11 +21,14 @@ export default function agentBackend(ctx) {
|
|
|
18
21
|
if (p)
|
|
19
22
|
providerRegistry.set(name, p);
|
|
20
23
|
}
|
|
24
|
+
const providerHooks = new Map();
|
|
21
25
|
const buildModes = () => {
|
|
22
26
|
const allModes = [];
|
|
23
27
|
for (const [id, p] of providerRegistry) {
|
|
24
28
|
if (!p.apiKey)
|
|
25
29
|
continue;
|
|
30
|
+
const shapeId = p.reasoningShape ?? id;
|
|
31
|
+
const buildReasoningParams = providerHooks.get(shapeId)?.reasoningParams ?? defaultReasoningBuilder;
|
|
26
32
|
for (const model of p.models) {
|
|
27
33
|
const mc = p.modelCapabilities?.get(model);
|
|
28
34
|
allModes.push({
|
|
@@ -33,6 +39,7 @@ export default function agentBackend(ctx) {
|
|
|
33
39
|
reasoning: mc?.reasoning,
|
|
34
40
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
35
41
|
echoReasoning: mc?.echoReasoning,
|
|
42
|
+
buildReasoningParams,
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
}
|
|
@@ -67,6 +74,7 @@ export default function agentBackend(ctx) {
|
|
|
67
74
|
initialModeIndex,
|
|
68
75
|
compositor: ctx.compositor,
|
|
69
76
|
instanceId: ctx.instanceId,
|
|
77
|
+
history: config.history,
|
|
70
78
|
});
|
|
71
79
|
bus.on("core:extensions-loaded", () => {
|
|
72
80
|
const settings = getSettings();
|
|
@@ -126,6 +134,12 @@ export default function agentBackend(ctx) {
|
|
|
126
134
|
},
|
|
127
135
|
});
|
|
128
136
|
});
|
|
137
|
+
bus.on("provider:configure", ({ id, reasoningParams }) => {
|
|
138
|
+
const prev = providerHooks.get(id) ?? {};
|
|
139
|
+
if (reasoningParams !== undefined)
|
|
140
|
+
prev.reasoningParams = reasoningParams;
|
|
141
|
+
providerHooks.set(id, prev);
|
|
142
|
+
});
|
|
129
143
|
bus.on("provider:register", (p) => {
|
|
130
144
|
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
131
145
|
const modelIds = [];
|
|
@@ -148,6 +162,7 @@ export default function agentBackend(ctx) {
|
|
|
148
162
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
149
163
|
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
150
164
|
});
|
|
165
|
+
const buildReasoningParams = providerHooks.get(p.id)?.reasoningParams ?? defaultReasoningBuilder;
|
|
151
166
|
const addModes = modelIds.map((m) => {
|
|
152
167
|
const mc = caps.get(m);
|
|
153
168
|
return {
|
|
@@ -158,6 +173,7 @@ export default function agentBackend(ctx) {
|
|
|
158
173
|
reasoning: mc?.reasoning,
|
|
159
174
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
160
175
|
echoReasoning: mc?.echoReasoning,
|
|
176
|
+
buildReasoningParams,
|
|
161
177
|
};
|
|
162
178
|
});
|
|
163
179
|
bus.emit("config:add-modes", { modes: addModes });
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in OpenAI-compatible provider
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Built-in OpenAI-compatible provider. Two activation paths:
|
|
3
|
+
* - OPENAI_API_KEY only → cloud OpenAI, ships a curated catalog.
|
|
4
|
+
* - OPENAI_BASE_URL (any key) → local/3rd-party server (Ollama, LM Studio,
|
|
5
|
+
* vLLM, llama.cpp); the catalog is fetched
|
|
6
|
+
* from the server's /models endpoint.
|
|
5
7
|
*/
|
|
6
8
|
import type { ExtensionContext } from "../types.js";
|
|
7
9
|
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -1,34 +1,36 @@
|
|
|
1
|
-
const
|
|
2
|
-
"gpt-5",
|
|
3
|
-
"gpt-4.1",
|
|
4
|
-
"gpt-4o",
|
|
5
|
-
"gpt-4o-mini",
|
|
6
|
-
"o3",
|
|
7
|
-
"o3-mini",
|
|
1
|
+
const OPENAI_CLOUD_MODELS = [
|
|
2
|
+
{ id: "gpt-5", reasoning: true },
|
|
3
|
+
{ id: "gpt-4.1", reasoning: false },
|
|
4
|
+
{ id: "gpt-4o", reasoning: false },
|
|
5
|
+
{ id: "gpt-4o-mini", reasoning: false },
|
|
6
|
+
{ id: "o3", reasoning: true },
|
|
7
|
+
{ id: "o3-mini", reasoning: true },
|
|
8
8
|
];
|
|
9
9
|
export default function activate(ctx) {
|
|
10
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
11
|
-
if (!apiKey)
|
|
12
|
-
return;
|
|
10
|
+
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
13
11
|
const baseURL = process.env.OPENAI_BASE_URL;
|
|
14
|
-
const id = baseURL ? "openai-compatible" : "openai";
|
|
15
12
|
if (!baseURL) {
|
|
13
|
+
if (!apiKey)
|
|
14
|
+
return;
|
|
16
15
|
ctx.bus.emit("provider:register", {
|
|
17
|
-
id,
|
|
16
|
+
id: "openai",
|
|
18
17
|
apiKey,
|
|
19
|
-
defaultModel:
|
|
20
|
-
models:
|
|
18
|
+
defaultModel: OPENAI_CLOUD_MODELS[0].id,
|
|
19
|
+
models: OPENAI_CLOUD_MODELS,
|
|
21
20
|
});
|
|
22
21
|
return;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const id = "openai-compatible";
|
|
24
|
+
// Local servers (Ollama, llama.cpp) often need no key; the SDK still
|
|
25
|
+
// requires a non-empty string for construction.
|
|
26
|
+
const sdkKey = apiKey || "no-key";
|
|
27
|
+
ctx.bus.emit("provider:register", { id, apiKey: sdkKey, baseURL, models: [] });
|
|
26
28
|
fetchModels(baseURL, apiKey).then((models) => {
|
|
27
29
|
if (models.length === 0)
|
|
28
30
|
return;
|
|
29
31
|
ctx.bus.emit("provider:register", {
|
|
30
32
|
id,
|
|
31
|
-
apiKey,
|
|
33
|
+
apiKey: sdkKey,
|
|
32
34
|
baseURL,
|
|
33
35
|
defaultModel: models[0],
|
|
34
36
|
models,
|
|
@@ -36,9 +38,10 @@ export default function activate(ctx) {
|
|
|
36
38
|
}).catch(() => { });
|
|
37
39
|
}
|
|
38
40
|
async function fetchModels(baseURL, apiKey) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const headers = {};
|
|
42
|
+
if (apiKey)
|
|
43
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
44
|
+
const res = await fetch(`${baseURL.replace(/\/$/, "")}/models`, { headers });
|
|
42
45
|
if (!res.ok)
|
|
43
46
|
return [];
|
|
44
47
|
const data = await res.json();
|
|
@@ -6,10 +6,16 @@ const DEFAULT_MODELS = ["deepseek/deepseek-v4-flash"];
|
|
|
6
6
|
// providers.openrouter.echoReasoningPatterns = ["deepseek", "..."]
|
|
7
7
|
// providers.openrouter.models[*].echoReasoning = true | false
|
|
8
8
|
const BUILTIN_ECHO_REASONING_PATTERNS = [/deepseek/i];
|
|
9
|
+
function buildReasoningParams(level) {
|
|
10
|
+
return level === "off"
|
|
11
|
+
? { reasoning: { enabled: false } }
|
|
12
|
+
: { reasoning: { effort: level } };
|
|
13
|
+
}
|
|
9
14
|
export default function activate(ctx) {
|
|
10
15
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
11
16
|
if (!apiKey)
|
|
12
17
|
return;
|
|
18
|
+
ctx.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
|
|
13
19
|
ctx.bus.emit("provider:register", {
|
|
14
20
|
id: "openrouter",
|
|
15
21
|
apiKey,
|
package/dist/settings.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export interface ProviderConfig {
|
|
|
27
27
|
/** Case-insensitive regex sources matched against model id; matches default
|
|
28
28
|
* to echoReasoning=true. Per-model echoReasoning still wins. */
|
|
29
29
|
echoReasoningPatterns?: string[];
|
|
30
|
+
/** Borrow another registered provider's reasoning request shape by id
|
|
31
|
+
* (e.g. "openrouter"). Defaults to OpenAI-compat. */
|
|
32
|
+
reasoningShape?: string;
|
|
30
33
|
}
|
|
31
34
|
export interface Settings {
|
|
32
35
|
/** Extensions to load (npm packages or file paths). */
|
|
@@ -145,6 +148,8 @@ export interface ResolvedProvider {
|
|
|
145
148
|
contextWindow?: number;
|
|
146
149
|
echoReasoning?: boolean;
|
|
147
150
|
}>;
|
|
151
|
+
/** Borrow another registered provider's reasoning request shape by id. */
|
|
152
|
+
reasoningShape?: string;
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* Resolve a provider config by name from settings.
|
package/dist/settings.js
CHANGED
|
@@ -161,6 +161,7 @@ export function resolveProvider(name) {
|
|
|
161
161
|
models: modelIds.length ? modelIds : (defaultModel ? [defaultModel] : []),
|
|
162
162
|
contextWindow: provider.contextWindow,
|
|
163
163
|
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
164
|
+
reasoningShape: provider.reasoningShape,
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
/** Get all configured provider names. */
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils
|
|
|
5
5
|
import type { ToolDefinition } from "./agent/types.js";
|
|
6
6
|
import type { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
7
7
|
import type { Compositor } from "./utils/compositor.js";
|
|
8
|
+
import type { HistoryAdapter } from "./agent/history-file.js";
|
|
8
9
|
export type { ContentBlock } from "./event-bus.js";
|
|
9
10
|
export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
10
11
|
export type { RenderSurface } from "./utils/compositor.js";
|
|
@@ -51,6 +52,7 @@ export interface AgentMode {
|
|
|
51
52
|
/** Echo reasoning_content back on assistant turns. Required by DeepSeek;
|
|
52
53
|
* default off (leaky shims may forward it to the model as OOD input). */
|
|
53
54
|
echoReasoning?: boolean;
|
|
55
|
+
buildReasoningParams?: (level: string) => Record<string, unknown>;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Backend-agnostic LLM interface exposed via `ctx.llm`. Backends fulfill it
|
|
@@ -87,6 +89,8 @@ export interface AgentShellConfig {
|
|
|
87
89
|
baseURL?: string;
|
|
88
90
|
/** Named provider to use from settings.json. */
|
|
89
91
|
provider?: string;
|
|
92
|
+
/** Conversation history backend. Defaults to the on-disk HistoryFile. */
|
|
93
|
+
history?: HistoryAdapter;
|
|
90
94
|
}
|
|
91
95
|
/**
|
|
92
96
|
* Context passed to user/third-party extensions.
|
|
@@ -130,6 +134,11 @@ export interface ExtensionContext {
|
|
|
130
134
|
registerSkill: (name: string, description: string, filePath: string) => void;
|
|
131
135
|
/** Remove a registered skill by name. */
|
|
132
136
|
removeSkill: (name: string) => void;
|
|
137
|
+
providers: {
|
|
138
|
+
configure: (id: string, opts: {
|
|
139
|
+
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
140
|
+
}) => void;
|
|
141
|
+
};
|
|
133
142
|
llm: LlmInterface;
|
|
134
143
|
/** Register a named handler. */
|
|
135
144
|
define: (name: string, fn: (...args: any[]) => any) => void;
|