agent-sh 0.12.1 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/agent/agent-loop.js +34 -16
- package/dist/agent/conversation-state.d.ts +4 -0
- package/dist/agent/conversation-state.js +44 -0
- package/dist/agent/skills.js +2 -2
- package/dist/agent/system-prompt.js +2 -3
- package/dist/agent/tools/bash.js +10 -3
- package/dist/agent/types.d.ts +3 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.js +5 -3
- package/dist/event-bus.d.ts +22 -0
- package/dist/event-bus.js +51 -3
- package/dist/extension-loader.js +1 -0
- package/dist/extensions/agent-backend.js +4 -1
- package/dist/extensions/openrouter.js +32 -0
- package/dist/index.js +1 -0
- package/dist/init.js +1 -2
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +7 -3
- package/dist/shell/input-handler.d.ts +8 -18
- package/dist/shell/input-handler.js +57 -227
- package/dist/shell/output-parser.d.ts +2 -1
- package/dist/shell/output-parser.js +33 -18
- package/dist/shell/shell.d.ts +1 -0
- package/dist/shell/shell.js +9 -7
- package/dist/shell/tui-input-view.d.ts +37 -0
- package/dist/shell/tui-input-view.js +140 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/compositor.d.ts +7 -1
- package/dist/utils/compositor.js +13 -1
- package/dist/utils/floating-panel.d.ts +6 -2
- package/dist/utils/floating-panel.js +17 -17
- package/dist/utils/ref-counter.d.ts +9 -0
- package/dist/utils/ref-counter.js +9 -0
- package/package.json +3 -1
- package/dist/utils/frame-renderer.d.ts +0 -26
- package/dist/utils/frame-renderer.js +0 -76
- package/dist/utils/output-writer.d.ts +0 -36
- package/dist/utils/output-writer.js +0 -45
package/README.md
CHANGED
|
@@ -23,16 +23,22 @@ I still use Claude Code and pi for serious coding work — this doesn't replace
|
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
|
-
Install
|
|
26
|
+
Install from npm:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm install -g
|
|
29
|
+
npm install -g agent-sh
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Re-run the same command to update. Patch releases ship frequently; `npm update -g agent-sh` works too.
|
|
33
|
+
|
|
34
|
+
For unreleased changes on `main`, clone and link locally — this avoids `npm install -g github:...`, which builds on your machine and requires a working TypeScript toolchain:
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
|
-
|
|
37
|
+
git clone https://github.com/guanyilun/agent-sh.git
|
|
38
|
+
cd agent-sh
|
|
39
|
+
npm install # installs devDependencies (typescript, etc.)
|
|
40
|
+
npm run build # produces dist/
|
|
41
|
+
npm link # exposes `agent-sh` globally
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
Pick one of the zero-config paths below — no settings file needed. agent-sh auto-activates a built-in provider when it sees a known key.
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -29,6 +29,16 @@ import { discoverGlobalSkills, discoverProjectSkills } from "./skills.js";
|
|
|
29
29
|
* the LLM via the API `tools` param (or via load_tool in deferred-
|
|
30
30
|
* lookup mode) — this only trims the always-visible catalog.
|
|
31
31
|
*/
|
|
32
|
+
/** Reject on abort; orphaned `p` keeps running but its result is dropped. */
|
|
33
|
+
function raceAbort(p, signal) {
|
|
34
|
+
if (signal.aborted)
|
|
35
|
+
return Promise.reject(new Error("cancelled"));
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const onAbort = () => reject(new Error("cancelled"));
|
|
38
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
p.then((v) => { signal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { signal.removeEventListener("abort", onAbort); reject(e); });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
32
42
|
function summarizeDescription(desc) {
|
|
33
43
|
const firstLine = desc.split("\n", 1)[0];
|
|
34
44
|
const sentenceEnd = firstLine.search(/[.!?](\s|$)/);
|
|
@@ -817,12 +827,11 @@ export class AgentLoop {
|
|
|
817
827
|
this.conversation.addSystemNote(text);
|
|
818
828
|
this.bus.emit("conversation:message-appended", { role: "system", content: text });
|
|
819
829
|
});
|
|
830
|
+
// Fires on user-abort; extensions advise per tool name for cleanup.
|
|
831
|
+
h.define("tool:cancel", (_ctx) => { });
|
|
820
832
|
// Wraps each tool call: permission → execute → emit events.
|
|
821
|
-
// Extensions advise to add safe-mode, logging, metrics, custom policies.
|
|
822
|
-
// The ctx.onChunk callback is exposed so advisors can wrap it to
|
|
823
|
-
// intercept/transform streamed tool output (e.g. secret redaction).
|
|
824
833
|
h.define("tool:execute", async (ctx) => {
|
|
825
|
-
const { name, id, args, tool } = ctx;
|
|
834
|
+
const { name, id, args, tool, signal } = ctx;
|
|
826
835
|
// Validate required input fields before display/permission/execute.
|
|
827
836
|
// Some models emit wrong arg names (e.g. `file_path` instead of `path`),
|
|
828
837
|
// and downstream helpers assume required strings are present.
|
|
@@ -918,16 +927,21 @@ export class AgentLoop {
|
|
|
918
927
|
const onChunk = (tool.showOutput !== false && !diffShown)
|
|
919
928
|
? ctx.onChunk
|
|
920
929
|
: undefined;
|
|
921
|
-
const toolCtx =
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
// instead of the throw killing the whole turn.
|
|
930
|
+
const toolCtx = { signal };
|
|
931
|
+
if (this.compositor) {
|
|
932
|
+
toolCtx.ui = createToolUI(this.bus, this.compositor.surface("agent"));
|
|
933
|
+
}
|
|
926
934
|
let result;
|
|
927
935
|
try {
|
|
928
|
-
result = await tool.execute(args, onChunk, toolCtx);
|
|
936
|
+
result = await raceAbort(tool.execute(args, onChunk, toolCtx), signal);
|
|
929
937
|
}
|
|
930
938
|
catch (err) {
|
|
939
|
+
if (signal.aborted) {
|
|
940
|
+
try {
|
|
941
|
+
this.handlers.call("tool:cancel", { name, args, reason: "user-aborted" });
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
944
|
+
}
|
|
931
945
|
const message = err instanceof Error ? err.message : String(err);
|
|
932
946
|
result = { content: message, exitCode: 1, isError: true };
|
|
933
947
|
}
|
|
@@ -1169,7 +1183,8 @@ export class AgentLoop {
|
|
|
1169
1183
|
this.bus.emit("agent:tool-output-chunk", { chunk });
|
|
1170
1184
|
};
|
|
1171
1185
|
const result = await this.handlers.call("tool:execute", { name: tc.name, id: tc.id, args, tool, onChunk: defaultOnChunk,
|
|
1172
|
-
batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined
|
|
1186
|
+
batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined,
|
|
1187
|
+
signal });
|
|
1173
1188
|
// Truncate large outputs to avoid blowing context
|
|
1174
1189
|
let content = result.content;
|
|
1175
1190
|
const maxBytes = 16_384; // ~4k tokens
|
|
@@ -1584,12 +1599,15 @@ export class AgentLoop {
|
|
|
1584
1599
|
tc.argumentsJson = "{}";
|
|
1585
1600
|
}
|
|
1586
1601
|
}
|
|
1602
|
+
// Echo reasoning only for modes that opt in (e.g. DeepSeek-R1).
|
|
1587
1603
|
const extras = {};
|
|
1588
|
-
if (
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
.
|
|
1604
|
+
if (this.currentMode.echoReasoning) {
|
|
1605
|
+
if (reasoning && reasoningField)
|
|
1606
|
+
extras[reasoningField] = reasoning;
|
|
1607
|
+
if (reasoningDetailsByIndex.size > 0) {
|
|
1608
|
+
extras.reasoning_details = [...reasoningDetailsByIndex.entries()]
|
|
1609
|
+
.sort((a, b) => a[0] - b[0]).map(([, v]) => v);
|
|
1610
|
+
}
|
|
1593
1611
|
}
|
|
1594
1612
|
return {
|
|
1595
1613
|
text,
|
|
@@ -38,6 +38,7 @@ export declare class ConversationState {
|
|
|
38
38
|
private nextSeq;
|
|
39
39
|
private lastApiTokenCount;
|
|
40
40
|
private lastApiMessageCount;
|
|
41
|
+
private pendingNotes;
|
|
41
42
|
constructor(handlers?: HandlerFunctions, instanceId?: string);
|
|
42
43
|
/** Get JSON.stringify of messages, cached until next mutation. */
|
|
43
44
|
private getMessagesJson;
|
|
@@ -53,7 +54,10 @@ export declare class ConversationState {
|
|
|
53
54
|
addToolResult(toolCallId: string, content: string, isError?: boolean): void;
|
|
54
55
|
/** Add tool results as a user message (for inline tool protocol). */
|
|
55
56
|
addToolResultInline(content: string): void;
|
|
57
|
+
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
56
58
|
addSystemNote(text: string): void;
|
|
59
|
+
private hasOpenToolCalls;
|
|
60
|
+
private flushPendingNotes;
|
|
57
61
|
getMessages(): ChatCompletionMessageParam[];
|
|
58
62
|
/**
|
|
59
63
|
* If a stream was interrupted mid-tool-execution, an assistant message
|
|
@@ -56,6 +56,10 @@ export class ConversationState {
|
|
|
56
56
|
nextSeq = 1;
|
|
57
57
|
lastApiTokenCount = null;
|
|
58
58
|
lastApiMessageCount = 0;
|
|
59
|
+
// Notes queued when addSystemNote fires mid-tool-pair; flushed once
|
|
60
|
+
// the trailing tool_result lands. Splicing into the gap breaks
|
|
61
|
+
// reasoning_content pairing and is rejected by strict providers.
|
|
62
|
+
pendingNotes = [];
|
|
59
63
|
constructor(handlers, instanceId = "0000") {
|
|
60
64
|
this.handlers = handlers ?? null;
|
|
61
65
|
this.instanceId = instanceId;
|
|
@@ -100,16 +104,54 @@ export class ConversationState {
|
|
|
100
104
|
if (isError)
|
|
101
105
|
this.toolErrors.add(toolCallId);
|
|
102
106
|
this.invalidateMessagesCache();
|
|
107
|
+
this.flushPendingNotes();
|
|
103
108
|
}
|
|
104
109
|
/** Add tool results as a user message (for inline tool protocol). */
|
|
105
110
|
addToolResultInline(content) {
|
|
106
111
|
this.messages.push({ role: "user", content });
|
|
107
112
|
this.invalidateMessagesCache();
|
|
113
|
+
this.flushPendingNotes();
|
|
108
114
|
}
|
|
115
|
+
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
109
116
|
addSystemNote(text) {
|
|
117
|
+
if (this.hasOpenToolCalls()) {
|
|
118
|
+
this.pendingNotes.push(text);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
110
121
|
this.messages.push({ role: "user", content: text });
|
|
111
122
|
this.invalidateMessagesCache();
|
|
112
123
|
}
|
|
124
|
+
hasOpenToolCalls() {
|
|
125
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
126
|
+
const msg = this.messages[i];
|
|
127
|
+
if (msg.role === "tool")
|
|
128
|
+
continue;
|
|
129
|
+
if (msg.role !== "assistant")
|
|
130
|
+
return false;
|
|
131
|
+
if (!("tool_calls" in msg) || !msg.tool_calls)
|
|
132
|
+
return false;
|
|
133
|
+
const answered = new Set();
|
|
134
|
+
for (let j = i + 1; j < this.messages.length; j++) {
|
|
135
|
+
const m = this.messages[j];
|
|
136
|
+
if (m.role !== "tool")
|
|
137
|
+
break;
|
|
138
|
+
answered.add(m.tool_call_id);
|
|
139
|
+
}
|
|
140
|
+
return msg.tool_calls.some((tc) => !answered.has(tc.id));
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
flushPendingNotes() {
|
|
145
|
+
if (this.pendingNotes.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
if (this.hasOpenToolCalls())
|
|
148
|
+
return;
|
|
149
|
+
for (const text of this.pendingNotes) {
|
|
150
|
+
this.messages.push({ role: "user", content: text });
|
|
151
|
+
}
|
|
152
|
+
this.pendingNotes = [];
|
|
153
|
+
this.invalidateMessagesCache();
|
|
154
|
+
}
|
|
113
155
|
getMessages() {
|
|
114
156
|
return this.normalizeReasoningConsistency(this.stubDanglingToolCalls(this.messages));
|
|
115
157
|
}
|
|
@@ -175,6 +217,7 @@ export class ConversationState {
|
|
|
175
217
|
this.invalidateMessagesCache();
|
|
176
218
|
this.lastApiTokenCount = null;
|
|
177
219
|
this.lastApiMessageCount = 0;
|
|
220
|
+
this.flushPendingNotes();
|
|
178
221
|
}
|
|
179
222
|
pruneToolErrors() {
|
|
180
223
|
if (this.toolErrors.size === 0)
|
|
@@ -474,6 +517,7 @@ export class ConversationState {
|
|
|
474
517
|
this.nuclearEntries = [];
|
|
475
518
|
this.nuclearBySeq.clear();
|
|
476
519
|
this.recallArchive.clear();
|
|
520
|
+
this.pendingNotes = [];
|
|
477
521
|
this.invalidateMessagesCache();
|
|
478
522
|
this.lastApiTokenCount = null;
|
|
479
523
|
this.lastApiMessageCount = 0;
|
package/dist/agent/skills.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import * as os from "node:os";
|
|
16
|
-
import { getSettings } from "../settings.js";
|
|
16
|
+
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
17
17
|
/** Parse YAML frontmatter from a SKILL.md file. */
|
|
18
18
|
function parseFrontmatter(content) {
|
|
19
19
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
@@ -133,7 +133,7 @@ export function discoverGlobalSkills() {
|
|
|
133
133
|
return _cachedGlobalSkills;
|
|
134
134
|
const seen = new Set();
|
|
135
135
|
const skills = [];
|
|
136
|
-
addUnique(skills, scanDir(path.join(
|
|
136
|
+
addUnique(skills, scanDir(path.join(CONFIG_DIR, "skills")), seen);
|
|
137
137
|
const settings = getSettings();
|
|
138
138
|
for (const p of settings.skillPaths ?? []) {
|
|
139
139
|
addUnique(skills, scanDir(path.resolve(expandHome(p))), seen);
|
|
@@ -14,9 +14,8 @@ export function formatSkillsBlock(skills) {
|
|
|
14
14
|
+ "Load a skill's full content with read_file on its file path when needed.\n\n"
|
|
15
15
|
+ skills.map(s => `- **${s.name}**: ${s.description}\n Path: ${s.filePath}`).join("\n\n");
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const GLOBAL_AGENTS_MD = path.join(os.homedir(), ".agent-sh", "AGENTS.md");
|
|
17
|
+
import { CONFIG_DIR } from "../settings.js";
|
|
18
|
+
const GLOBAL_AGENTS_MD = path.join(CONFIG_DIR, "AGENTS.md");
|
|
20
19
|
// ── File caches ─────────────────────────────────────────────────────
|
|
21
20
|
// Convention files (CLAUDE.md/AGENT.md) are walked synchronously from
|
|
22
21
|
// CWD to root on every query. In practice they almost never change,
|
package/dist/agent/tools/bash.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { executeCommand } from "../../executor.js";
|
|
1
|
+
import { executeCommand, killSession } from "../../executor.js";
|
|
2
2
|
export function createBashTool(opts) {
|
|
3
3
|
return {
|
|
4
4
|
name: "bash",
|
|
@@ -33,7 +33,7 @@ export function createBashTool(opts) {
|
|
|
33
33
|
icon: "▶",
|
|
34
34
|
locations: [],
|
|
35
35
|
}),
|
|
36
|
-
async execute(args, onChunk) {
|
|
36
|
+
async execute(args, onChunk, ctx) {
|
|
37
37
|
const command = args.command;
|
|
38
38
|
const timeout = (args.timeout ?? 60) * 1000;
|
|
39
39
|
// Let extensions intercept before execution
|
|
@@ -57,7 +57,14 @@ export function createBashTool(opts) {
|
|
|
57
57
|
timeout,
|
|
58
58
|
onOutput: onChunk,
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
const onAbort = () => killSession(session);
|
|
61
|
+
ctx?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
try {
|
|
63
|
+
await done;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
ctx?.signal?.removeEventListener("abort", onAbort);
|
|
67
|
+
}
|
|
61
68
|
const content = session.truncated
|
|
62
69
|
? `[output truncated, showing last portion]\n${session.output}`
|
|
63
70
|
: session.output;
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -64,7 +64,9 @@ export interface ToolUI {
|
|
|
64
64
|
}
|
|
65
65
|
/** Context passed to tool execute() as optional third parameter. */
|
|
66
66
|
export interface ToolExecutionContext {
|
|
67
|
-
ui
|
|
67
|
+
ui?: ToolUI;
|
|
68
|
+
/** Aborted on Ctrl-C — tools with subprocess work should listen and clean up. */
|
|
69
|
+
signal?: AbortSignal;
|
|
68
70
|
}
|
|
69
71
|
export interface ToolDefinition {
|
|
70
72
|
name: string;
|
package/dist/core.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface AgentShellCore {
|
|
|
33
33
|
contextManager: ContextManager;
|
|
34
34
|
/** Handler registry for define/advise/call. */
|
|
35
35
|
handlers: HandlerRegistry;
|
|
36
|
+
/** Unique id for this agent process; used for shell-marker tagging and lineage tracking. */
|
|
37
|
+
instanceId: string;
|
|
36
38
|
/** Activate the agent backend (call after extensions load). */
|
|
37
39
|
activateBackend(): void;
|
|
38
40
|
/** Convenience: emit agent:submit and await the response. */
|
package/dist/core.js
CHANGED
|
@@ -27,9 +27,8 @@ import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
|
27
27
|
import crypto from "node:crypto";
|
|
28
28
|
import * as fs from "node:fs";
|
|
29
29
|
import * as path from "node:path";
|
|
30
|
-
import * as os from "node:os";
|
|
31
30
|
import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
|
|
32
|
-
|
|
31
|
+
import { CONFIG_DIR } from "./settings.js";
|
|
33
32
|
// Re-export types that library consumers need
|
|
34
33
|
export { EventBus } from "./event-bus.js";
|
|
35
34
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -43,6 +42,7 @@ export function createCore(config) {
|
|
|
43
42
|
// short enough to read/remember. Legacy content may have 16-char iids; any
|
|
44
43
|
// parsers should accept ≥6 hex chars.
|
|
45
44
|
const instanceId = crypto.randomBytes(3).toString("hex");
|
|
45
|
+
bus.setSource(instanceId);
|
|
46
46
|
const settings = settingsMod.getSettings();
|
|
47
47
|
// Expose raw CLI config so the agent backend extension can resolve
|
|
48
48
|
// providers and create the LLM client.
|
|
@@ -107,6 +107,7 @@ export function createCore(config) {
|
|
|
107
107
|
bus,
|
|
108
108
|
contextManager,
|
|
109
109
|
handlers,
|
|
110
|
+
instanceId,
|
|
110
111
|
activateBackend() {
|
|
111
112
|
// Silent — backend info is shown in the startup banner.
|
|
112
113
|
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
@@ -169,7 +170,7 @@ export function createCore(config) {
|
|
|
169
170
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
170
171
|
getExtensionSettings: settingsMod.getExtensionSettings,
|
|
171
172
|
getStoragePath: (namespace) => {
|
|
172
|
-
const dir = path.join(
|
|
173
|
+
const dir = path.join(CONFIG_DIR, namespace);
|
|
173
174
|
fs.mkdirSync(dir, { recursive: true });
|
|
174
175
|
return dir;
|
|
175
176
|
},
|
|
@@ -187,6 +188,7 @@ export function createCore(config) {
|
|
|
187
188
|
list: () => handlers.list(),
|
|
188
189
|
get terminalBuffer() { return getTerminalBuffer(); },
|
|
189
190
|
compositor,
|
|
191
|
+
onDispose: () => { },
|
|
190
192
|
createRemoteSession: (opts) => {
|
|
191
193
|
const { surface } = opts;
|
|
192
194
|
const cleanups = [];
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -291,6 +291,7 @@ export interface ShellEvents {
|
|
|
291
291
|
id: string;
|
|
292
292
|
reasoning?: boolean;
|
|
293
293
|
contextWindow?: number;
|
|
294
|
+
echoReasoning?: boolean;
|
|
294
295
|
})[];
|
|
295
296
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
296
297
|
supportsReasoningEffort?: boolean;
|
|
@@ -357,6 +358,14 @@ export type ContentBlock = {
|
|
|
357
358
|
type Listener<T> = (payload: T) => void;
|
|
358
359
|
type PipeListener<T> = (payload: T) => T;
|
|
359
360
|
type AsyncPipeListener<T> = (payload: T) => T | Promise<T>;
|
|
361
|
+
/** Envelope stamped on every emitted event. */
|
|
362
|
+
export interface BusMeta {
|
|
363
|
+
source: string;
|
|
364
|
+
ts: number;
|
|
365
|
+
id: string;
|
|
366
|
+
name: string;
|
|
367
|
+
}
|
|
368
|
+
export type AnyListener = (name: string, payload: unknown, meta: BusMeta) => void;
|
|
360
369
|
/**
|
|
361
370
|
* Typed event bus with two modes:
|
|
362
371
|
* - emit/on/off: fire-and-forget notifications
|
|
@@ -367,12 +376,25 @@ export declare class EventBus {
|
|
|
367
376
|
private emitter;
|
|
368
377
|
private pipeListeners;
|
|
369
378
|
private asyncPipeListeners;
|
|
379
|
+
private source;
|
|
380
|
+
private nextSeq;
|
|
381
|
+
private anyListeners;
|
|
382
|
+
/** Set the source id stamped onto every emitted event. */
|
|
383
|
+
setSource(src: string): void;
|
|
384
|
+
/** Subscribe to every emitted event with full envelope. Returns unsubscribe. */
|
|
385
|
+
onAny(fn: AnyListener): () => void;
|
|
386
|
+
/** Stamp + dispatch — used by every emit path. */
|
|
387
|
+
private dispatch;
|
|
370
388
|
/** Subscribe to a fire-and-forget event. */
|
|
371
389
|
on<K extends keyof ShellEvents>(event: K, fn: Listener<ShellEvents[K]>): void;
|
|
372
390
|
/** Unsubscribe from a fire-and-forget event. */
|
|
373
391
|
off<K extends keyof ShellEvents>(event: K, fn: Listener<ShellEvents[K]>): void;
|
|
374
392
|
/** Emit a fire-and-forget event. */
|
|
375
393
|
emit<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
|
|
394
|
+
/** Re-dispatch an event with externally-supplied meta. Used by bridges
|
|
395
|
+
* and replay tools to preserve the original source/ts/id of remote or
|
|
396
|
+
* recorded events instead of restamping them as locally originated. */
|
|
397
|
+
relay(meta: BusMeta, payload: unknown): void;
|
|
376
398
|
/**
|
|
377
399
|
* Transform-then-notify: run the payload through any registered pipe
|
|
378
400
|
* listeners (transforms), then emit the final result to regular `on`
|
package/dist/event-bus.js
CHANGED
|
@@ -9,6 +9,40 @@ export class EventBus {
|
|
|
9
9
|
emitter = new EventEmitter().setMaxListeners(0);
|
|
10
10
|
pipeListeners = new Map();
|
|
11
11
|
asyncPipeListeners = new Map();
|
|
12
|
+
source = "0000";
|
|
13
|
+
nextSeq = 0;
|
|
14
|
+
anyListeners = [];
|
|
15
|
+
/** Set the source id stamped onto every emitted event. */
|
|
16
|
+
setSource(src) {
|
|
17
|
+
this.source = src;
|
|
18
|
+
}
|
|
19
|
+
/** Subscribe to every emitted event with full envelope. Returns unsubscribe. */
|
|
20
|
+
onAny(fn) {
|
|
21
|
+
this.anyListeners.push(fn);
|
|
22
|
+
return () => {
|
|
23
|
+
const i = this.anyListeners.indexOf(fn);
|
|
24
|
+
if (i !== -1)
|
|
25
|
+
this.anyListeners.splice(i, 1);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Stamp + dispatch — used by every emit path. */
|
|
29
|
+
dispatch(name, payload) {
|
|
30
|
+
if (this.anyListeners.length > 0) {
|
|
31
|
+
const meta = {
|
|
32
|
+
source: this.source,
|
|
33
|
+
ts: Date.now(),
|
|
34
|
+
id: `${this.source}:${this.nextSeq++}`,
|
|
35
|
+
name,
|
|
36
|
+
};
|
|
37
|
+
for (const fn of this.anyListeners) {
|
|
38
|
+
try {
|
|
39
|
+
fn(name, payload, meta);
|
|
40
|
+
}
|
|
41
|
+
catch { /* swallow */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.emitter.emit(name, payload);
|
|
45
|
+
}
|
|
12
46
|
/** Subscribe to a fire-and-forget event. */
|
|
13
47
|
on(event, fn) {
|
|
14
48
|
this.emitter.on(event, fn);
|
|
@@ -19,7 +53,21 @@ export class EventBus {
|
|
|
19
53
|
}
|
|
20
54
|
/** Emit a fire-and-forget event. */
|
|
21
55
|
emit(event, payload) {
|
|
22
|
-
this.
|
|
56
|
+
this.dispatch(event, payload);
|
|
57
|
+
}
|
|
58
|
+
/** Re-dispatch an event with externally-supplied meta. Used by bridges
|
|
59
|
+
* and replay tools to preserve the original source/ts/id of remote or
|
|
60
|
+
* recorded events instead of restamping them as locally originated. */
|
|
61
|
+
relay(meta, payload) {
|
|
62
|
+
if (this.anyListeners.length > 0) {
|
|
63
|
+
for (const fn of this.anyListeners) {
|
|
64
|
+
try {
|
|
65
|
+
fn(meta.name, payload, meta);
|
|
66
|
+
}
|
|
67
|
+
catch { /* swallow */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.emitter.emit(meta.name, payload);
|
|
23
71
|
}
|
|
24
72
|
/**
|
|
25
73
|
* Transform-then-notify: run the payload through any registered pipe
|
|
@@ -38,7 +86,7 @@ export class EventBus {
|
|
|
38
86
|
}
|
|
39
87
|
transformed = payload; // fall back to untransformed
|
|
40
88
|
}
|
|
41
|
-
this.
|
|
89
|
+
this.dispatch(event, transformed);
|
|
42
90
|
}
|
|
43
91
|
/** Register a transform listener for a pipeline event. */
|
|
44
92
|
onPipe(event, fn) {
|
|
@@ -103,7 +151,7 @@ export class EventBus {
|
|
|
103
151
|
*/
|
|
104
152
|
async emitPipeAsync(event, payload) {
|
|
105
153
|
// Phase 1: notify (lets renderers prepare for interactive I/O)
|
|
106
|
-
this.
|
|
154
|
+
this.dispatch(event, payload);
|
|
107
155
|
// Phase 2: transform (extensions provide decisions)
|
|
108
156
|
const listeners = this.asyncPipeListeners.get(event);
|
|
109
157
|
if (!listeners)
|
package/dist/extension-loader.js
CHANGED
|
@@ -96,6 +96,7 @@ function createScopedContext(ctx, extensionName) {
|
|
|
96
96
|
registerTool: scopedRegisterTool,
|
|
97
97
|
unregisterTool: ctx.unregisterTool,
|
|
98
98
|
registerCommand: scopedRegisterCommand,
|
|
99
|
+
onDispose: (fn) => { cleanups.push(fn); },
|
|
99
100
|
};
|
|
100
101
|
const dispose = () => {
|
|
101
102
|
for (const fn of cleanups) {
|
|
@@ -32,6 +32,7 @@ export default function agentBackend(ctx) {
|
|
|
32
32
|
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
33
33
|
reasoning: mc?.reasoning,
|
|
34
34
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
35
|
+
echoReasoning: mc?.echoReasoning,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -135,7 +136,7 @@ export default function agentBackend(ctx) {
|
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
138
|
modelIds.push(m.id);
|
|
138
|
-
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
139
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, echoReasoning: m.echoReasoning });
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
providerRegistry.set(p.id, {
|
|
@@ -156,6 +157,7 @@ export default function agentBackend(ctx) {
|
|
|
156
157
|
contextWindow: mc?.contextWindow,
|
|
157
158
|
reasoning: mc?.reasoning,
|
|
158
159
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
160
|
+
echoReasoning: mc?.echoReasoning,
|
|
159
161
|
};
|
|
160
162
|
});
|
|
161
163
|
bus.emit("config:add-modes", { modes: addModes });
|
|
@@ -197,6 +199,7 @@ export default function agentBackend(ctx) {
|
|
|
197
199
|
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
198
200
|
reasoning: mc?.reasoning,
|
|
199
201
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
202
|
+
echoReasoning: mc?.echoReasoning,
|
|
200
203
|
};
|
|
201
204
|
});
|
|
202
205
|
bus.emit("config:set-modes", { modes: newModes });
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { getSettings } from "../settings.js";
|
|
1
2
|
const BASE_URL = "https://openrouter.ai/api/v1";
|
|
2
3
|
const DEFAULT_MODELS = ["anthropic/claude-sonnet-4.6"];
|
|
4
|
+
// Built-in defaults for models requiring reasoning_content echoed back
|
|
5
|
+
// (server 400s without it). Extend or override in settings.json:
|
|
6
|
+
// providers.openrouter.echoReasoningPatterns = ["deepseek", "..."]
|
|
7
|
+
// providers.openrouter.models[*].echoReasoning = true | false
|
|
8
|
+
const BUILTIN_ECHO_REASONING_PATTERNS = [/deepseek/i];
|
|
3
9
|
export default function activate(ctx) {
|
|
4
10
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
5
11
|
if (!apiKey)
|
|
@@ -14,6 +20,8 @@ export default function activate(ctx) {
|
|
|
14
20
|
fetchModels(apiKey).then((models) => {
|
|
15
21
|
if (models.length === 0)
|
|
16
22
|
return;
|
|
23
|
+
const userOverrides = readUserOverrides();
|
|
24
|
+
const patterns = readEchoPatterns();
|
|
17
25
|
ctx.bus.emit("provider:register", {
|
|
18
26
|
id: "openrouter",
|
|
19
27
|
apiKey,
|
|
@@ -24,10 +32,34 @@ export default function activate(ctx) {
|
|
|
24
32
|
id: m.id,
|
|
25
33
|
reasoning: m.supported_parameters?.includes("reasoning") ?? false,
|
|
26
34
|
contextWindow: m.context_length,
|
|
35
|
+
echoReasoning: userOverrides.get(m.id) ?? patterns.some((re) => re.test(m.id)),
|
|
27
36
|
})),
|
|
28
37
|
});
|
|
29
38
|
}).catch(() => { });
|
|
30
39
|
}
|
|
40
|
+
function readEchoPatterns() {
|
|
41
|
+
const userPatterns = getSettings().providers?.openrouter?.echoReasoningPatterns ?? [];
|
|
42
|
+
const compiled = [];
|
|
43
|
+
for (const src of userPatterns) {
|
|
44
|
+
try {
|
|
45
|
+
compiled.push(new RegExp(src, "i"));
|
|
46
|
+
}
|
|
47
|
+
catch { /* skip invalid pattern */ }
|
|
48
|
+
}
|
|
49
|
+
return [...BUILTIN_ECHO_REASONING_PATTERNS, ...compiled];
|
|
50
|
+
}
|
|
51
|
+
function readUserOverrides() {
|
|
52
|
+
const out = new Map();
|
|
53
|
+
const models = getSettings().providers?.openrouter?.models;
|
|
54
|
+
if (!Array.isArray(models))
|
|
55
|
+
return out;
|
|
56
|
+
for (const m of models) {
|
|
57
|
+
if (typeof m === "object" && m && m.echoReasoning !== undefined) {
|
|
58
|
+
out.set(m.id, m.echoReasoning);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
31
63
|
async function fetchModels(apiKey) {
|
|
32
64
|
const res = await fetch(`${BASE_URL}/models`, {
|
|
33
65
|
headers: { Authorization: `Bearer ${apiKey}` },
|
package/dist/index.js
CHANGED
|
@@ -218,6 +218,7 @@ async function main() {
|
|
|
218
218
|
rows,
|
|
219
219
|
shell: config.shell || process.env.SHELL || "/bin/bash",
|
|
220
220
|
cwd: process.cwd(),
|
|
221
|
+
instanceId: core.instanceId,
|
|
221
222
|
onShowAgentInfo: () => {
|
|
222
223
|
if (agentInfo) {
|
|
223
224
|
return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
|
package/dist/init.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
const CONFIG_DIR = path.join(os.homedir(), ".agent-sh");
|
|
3
|
+
import { CONFIG_DIR } from "./settings.js";
|
|
5
4
|
const EXTENSIONS_DIR = path.join(CONFIG_DIR, "extensions");
|
|
6
5
|
const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
7
6
|
const EXAMPLE_PATH = path.join(CONFIG_DIR, "settings.example.json");
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Root config directory. Override via AGENT_SH_HOME for isolated instances
|
|
2
|
+
* (testing, multi-agent setups). Path is resolved at module load. */
|
|
1
3
|
export declare const CONFIG_DIR: string;
|
|
2
4
|
/** Per-model capability overrides. */
|
|
3
5
|
export interface ModelCapabilityConfig {
|
|
@@ -7,6 +9,8 @@ export interface ModelCapabilityConfig {
|
|
|
7
9
|
reasoning?: boolean;
|
|
8
10
|
/** Context window size in tokens for this specific model. */
|
|
9
11
|
contextWindow?: number;
|
|
12
|
+
/** Echo reasoning_content back on assistant turns. Required by DeepSeek. */
|
|
13
|
+
echoReasoning?: boolean;
|
|
10
14
|
}
|
|
11
15
|
/** Provider profile — a named LLM configuration. */
|
|
12
16
|
export interface ProviderConfig {
|
|
@@ -20,6 +24,9 @@ export interface ProviderConfig {
|
|
|
20
24
|
models?: (string | ModelCapabilityConfig)[];
|
|
21
25
|
/** Context window size in tokens (e.g. 128000). Used for usage display. */
|
|
22
26
|
contextWindow?: number;
|
|
27
|
+
/** Case-insensitive regex sources matched against model id; matches default
|
|
28
|
+
* to echoReasoning=true. Per-model echoReasoning still wins. */
|
|
29
|
+
echoReasoningPatterns?: string[];
|
|
23
30
|
}
|
|
24
31
|
export interface Settings {
|
|
25
32
|
/** Extensions to load (npm packages or file paths). */
|
|
@@ -136,6 +143,7 @@ export interface ResolvedProvider {
|
|
|
136
143
|
modelCapabilities?: Map<string, {
|
|
137
144
|
reasoning?: boolean;
|
|
138
145
|
contextWindow?: number;
|
|
146
|
+
echoReasoning?: boolean;
|
|
139
147
|
}>;
|
|
140
148
|
}
|
|
141
149
|
/**
|
package/dist/settings.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import * as os from "node:os";
|
|
10
|
-
|
|
10
|
+
/** Root config directory. Override via AGENT_SH_HOME for isolated instances
|
|
11
|
+
* (testing, multi-agent setups). Path is resolved at module load. */
|
|
12
|
+
export const CONFIG_DIR = process.env.AGENT_SH_HOME
|
|
13
|
+
? path.resolve(process.env.AGENT_SH_HOME)
|
|
14
|
+
: path.join(os.homedir(), ".agent-sh");
|
|
11
15
|
const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
12
16
|
const DEFAULTS = {
|
|
13
17
|
extensions: [],
|
|
@@ -143,8 +147,8 @@ export function resolveProvider(name) {
|
|
|
143
147
|
}
|
|
144
148
|
else {
|
|
145
149
|
modelIds.push(m.id);
|
|
146
|
-
if (m.reasoning !== undefined || m.contextWindow !== undefined) {
|
|
147
|
-
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
150
|
+
if (m.reasoning !== undefined || m.contextWindow !== undefined || m.echoReasoning !== undefined) {
|
|
151
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, echoReasoning: m.echoReasoning });
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
}
|