agent-sh 0.14.7 → 0.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-loop.d.ts +0 -4
- package/dist/agent/agent-loop.js +8 -166
- package/dist/agent/entry-format.d.ts +5 -0
- package/dist/agent/entry-format.js +9 -0
- package/dist/agent/extensions/rolling-history/constants.d.ts +1 -0
- package/dist/agent/extensions/rolling-history/constants.js +1 -0
- package/dist/agent/extensions/rolling-history/index.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/index.js +203 -0
- package/dist/agent/extensions/rolling-history/recall.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/recall.js +122 -0
- package/dist/agent/extensions/rolling-history/strategy.d.ts +70 -0
- package/dist/agent/extensions/rolling-history/strategy.js +336 -0
- package/dist/agent/host-types.d.ts +0 -3
- package/dist/agent/index.js +46 -5
- package/dist/agent/live-view.d.ts +57 -0
- package/dist/agent/live-view.js +238 -0
- package/dist/agent/llm-client.d.ts +1 -0
- package/dist/agent/llm-client.js +1 -1
- package/dist/agent/session-store.d.ts +90 -0
- package/dist/agent/session-store.js +288 -0
- package/dist/agent/store.d.ts +74 -0
- package/dist/agent/store.js +284 -0
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/tool-protocol.d.ts +11 -11
- package/dist/cli/auth/discover.js +18 -1
- package/dist/cli/index.js +4 -2
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -1
- package/dist/core/settings.d.ts +5 -1
- package/dist/core/settings.js +62 -1
- package/dist/extensions/index.d.ts +1 -0
- package/dist/shell/events.d.ts +1 -0
- package/dist/shell/input-handler.js +4 -0
- package/dist/shell/strategies/bash.js +6 -2
- package/dist/shell/tui-renderer.js +5 -2
- package/dist/utils/diff-renderer.js +9 -7
- package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/capture.ts +1 -1
- package/examples/extensions/ashi/src/cli.ts +3 -4
- package/examples/extensions/ashi/src/compaction.ts +6 -2
- package/examples/extensions/ashi/src/frontend.ts +13 -10
- package/examples/extensions/ashi/src/multi-session-store.ts +35 -12
- package/examples/extensions/ashi/src/session-commands.ts +1 -1
- package/examples/extensions/ashi/src/user-shell-intents.ts +17 -0
- package/examples/extensions/ollama.ts +3 -2
- package/examples/extensions/opencode-provider.ts +1 -2
- package/examples/extensions/zai-coding-plan.ts +1 -2
- package/package.json +13 -1
- package/dist/agent/conversation-state.d.ts +0 -142
- package/dist/agent/conversation-state.js +0 -788
- package/dist/agent/history-file.d.ts +0 -81
- package/dist/agent/history-file.js +0 -271
- package/examples/extensions/ashi/src/session-store.ts +0 -363
|
@@ -30,8 +30,12 @@ export const bashStrategy = {
|
|
|
30
30
|
`PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}__agent_sh_precmd"`,
|
|
31
31
|
"",
|
|
32
32
|
"# Preexec hook via DEBUG trap: emit actual command text so agent-sh",
|
|
33
|
-
"# can track history-recalled and tab-completed commands accurately",
|
|
34
|
-
"
|
|
33
|
+
"# can track history-recalled and tab-completed commands accurately.",
|
|
34
|
+
"# Start latched (=1) so the trap stays inert through the rest of",
|
|
35
|
+
"# rcfile sourcing — readline/history aren't loaded yet, and the case",
|
|
36
|
+
"# + bind statements below would otherwise fire a phantom preexec with",
|
|
37
|
+
"# an empty body. __agent_sh_precmd resets it to 0 before user input.",
|
|
38
|
+
"__agent_sh_preexec_ran=1",
|
|
35
39
|
"__agent_sh_emit_preexec() {",
|
|
36
40
|
' [[ $__agent_sh_preexec_ran == 1 ]] && return',
|
|
37
41
|
' [[ -n $COMP_LINE ]] && return',
|
|
@@ -408,11 +408,14 @@ export default function activate(ctx) {
|
|
|
408
408
|
bus.on("ui:info", (e) => {
|
|
409
409
|
stopCurrentSpinner();
|
|
410
410
|
showInfo(e.message);
|
|
411
|
-
|
|
411
|
+
bus.emit("input:redraw", {});
|
|
412
412
|
if (s.renderer)
|
|
413
413
|
startThinkingSpinner();
|
|
414
414
|
});
|
|
415
|
-
bus.on("ui:error", (e) =>
|
|
415
|
+
bus.on("ui:error", (e) => {
|
|
416
|
+
showError(e.message);
|
|
417
|
+
bus.emit("input:redraw", {});
|
|
418
|
+
});
|
|
416
419
|
bus.on("ui:suggestion", (e) => {
|
|
417
420
|
compositor.surface("status").writeLine(`${p.dim}💡 ${e.text}${p.reset}`);
|
|
418
421
|
});
|
|
@@ -249,13 +249,15 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
249
249
|
const out = [];
|
|
250
250
|
const pairs = findChangePairs(hunk);
|
|
251
251
|
const renderedAsPartOfPair = new Set();
|
|
252
|
+
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
253
|
+
const gutter = (n) => `${p.dim}${n} │${p.reset} `;
|
|
252
254
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
253
255
|
const line = hunk.lines[i];
|
|
254
256
|
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
255
257
|
if (line.type === "context") {
|
|
256
258
|
const raw = truncateText(line.text, lineTextW);
|
|
257
259
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
258
|
-
out.push(
|
|
260
|
+
out.push(`${gutter(no)} ${p.dim}${text}${p.reset}`);
|
|
259
261
|
continue;
|
|
260
262
|
}
|
|
261
263
|
if (line.type === "removed") {
|
|
@@ -275,17 +277,17 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
275
277
|
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
276
278
|
}
|
|
277
279
|
if (useTrueColor) {
|
|
278
|
-
out.push(padToWidth(`${p.errorBg}${p.error}- ${
|
|
280
|
+
out.push(gutter(no) + padToWidth(`${p.errorBg}${p.error}- ${preserveBg(removedText, p.errorBg)}`, bgWidth) + p.reset);
|
|
279
281
|
}
|
|
280
282
|
else {
|
|
281
|
-
out.push(`${p.error}- ${
|
|
283
|
+
out.push(`${gutter(no)}${p.error}- ${removedText}${p.reset}`);
|
|
282
284
|
}
|
|
283
285
|
if (addedText !== null && addedNo !== null) {
|
|
284
286
|
if (useTrueColor) {
|
|
285
|
-
out.push(padToWidth(`${p.successBg}${p.success}+ ${
|
|
287
|
+
out.push(gutter(addedNo) + padToWidth(`${p.successBg}${p.success}+ ${preserveBg(addedText, p.successBg)}`, bgWidth) + p.reset);
|
|
286
288
|
}
|
|
287
289
|
else {
|
|
288
|
-
out.push(`${p.success}+ ${
|
|
290
|
+
out.push(`${gutter(addedNo)}${p.success}+ ${addedText}${p.reset}`);
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
continue;
|
|
@@ -296,10 +298,10 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
296
298
|
const raw = truncateText(line.text, lineTextW);
|
|
297
299
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
298
300
|
if (useTrueColor) {
|
|
299
|
-
out.push(padToWidth(`${p.successBg}${p.success}+ ${
|
|
301
|
+
out.push(gutter(no) + padToWidth(`${p.successBg}${p.success}+ ${preserveBg(text, p.successBg)}`, bgWidth) + p.reset);
|
|
300
302
|
}
|
|
301
303
|
else {
|
|
302
|
-
out.push(`${p.success}+ ${
|
|
304
|
+
out.push(`${gutter(no)}${p.success}+ ${text}${p.reset}`);
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* In agent-shell (Emacs):
|
|
12
12
|
* (setq agent-shell-agentsh-acp-command '("agent-sh-acp"))
|
|
13
13
|
*/
|
|
14
|
-
import { createCore,
|
|
14
|
+
import { createCore, type AgentShellCore } from "agent-sh";
|
|
15
15
|
import { loadExtensions } from "agent-sh/extension-loader";
|
|
16
16
|
import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
17
17
|
import { activateAgent } from "agent-sh/agent";
|
|
@@ -486,7 +486,6 @@ async function handleSessionNew(id: number | string, params: Record<string, unkn
|
|
|
486
486
|
core = createCore({
|
|
487
487
|
model: cliArgs.model,
|
|
488
488
|
provider: cliArgs.provider,
|
|
489
|
-
history: new NoopHistory(),
|
|
490
489
|
});
|
|
491
490
|
wireEvents(core);
|
|
492
491
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guanyilun/ashi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Ash in an interactive TUI — agent-sh's built-in agent without the shell underneath",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@earendil-works/pi-tui": "^0.74.0",
|
|
59
|
-
"agent-sh": "^0.14.
|
|
59
|
+
"agent-sh": "^0.14.8",
|
|
60
60
|
"chalk": "^5.5.0",
|
|
61
61
|
"cli-highlight": "^2.1.11"
|
|
62
62
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "agent-sh/types";
|
|
2
2
|
import type { MultiSessionStore } from "./multi-session-store.js";
|
|
3
|
-
import type { AgentMessage } from "
|
|
3
|
+
import type { AgentShMessage as AgentMessage } from "agent-sh/session-store";
|
|
4
4
|
|
|
5
5
|
/** Maintains an `(entryId | null)[]` parallel to the live messages array;
|
|
6
6
|
* null slots are synthetics like compaction summaries that have no entry. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ashi — ash (agent-sh's built-in agent) in an interactive TUI.
|
|
4
4
|
*/
|
|
5
|
-
import { createCore
|
|
5
|
+
import { createCore } from "agent-sh/core";
|
|
6
6
|
import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
7
7
|
import { loadExtensions } from "agent-sh/extension-loader";
|
|
8
8
|
import { activateAgent } from "agent-sh/agent";
|
|
@@ -134,7 +134,7 @@ async function main(): Promise<void> {
|
|
|
134
134
|
const store = new MultiSessionStore(sessionsDir, cwd, { resumeSessionId: resumeId });
|
|
135
135
|
const getStore = (): MultiSessionStore => store;
|
|
136
136
|
|
|
137
|
-
const core = createCore(
|
|
137
|
+
const core = createCore(config);
|
|
138
138
|
|
|
139
139
|
let stopFrontend: (() => void) | null = null;
|
|
140
140
|
|
|
@@ -153,7 +153,7 @@ async function main(): Promise<void> {
|
|
|
153
153
|
|
|
154
154
|
activateAgent(ctx);
|
|
155
155
|
activateShellContext(ctx);
|
|
156
|
-
await loadBuiltinExtensions(ctx);
|
|
156
|
+
await loadBuiltinExtensions(ctx, ["rolling-history"]);
|
|
157
157
|
|
|
158
158
|
const shell = new Shell({
|
|
159
159
|
bus: core.bus,
|
|
@@ -183,7 +183,6 @@ async function main(): Promise<void> {
|
|
|
183
183
|
registerRenderDefaults(ctx);
|
|
184
184
|
registerDefaultSchemaRenderers(ctx);
|
|
185
185
|
|
|
186
|
-
ctx.advise("conversation:format-prior-history", () => null);
|
|
187
186
|
ctx.advise("system-prompt:build", (next) => `${next()}\n\n<cwd>${process.cwd()}</cwd>`);
|
|
188
187
|
|
|
189
188
|
const handle = mountAshi(ctx, getStore, capture);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "agent-sh/types";
|
|
2
2
|
import type { MultiSessionStore } from "./multi-session-store.js";
|
|
3
3
|
import type { Capture } from "./capture.js";
|
|
4
|
-
import type { AgentMessage } from "
|
|
4
|
+
import type { AgentShMessage as AgentMessage } from "agent-sh/session-store";
|
|
5
5
|
|
|
6
6
|
const KEEP_RECENT_TOKEN_BUDGET = 20_000;
|
|
7
7
|
const FORCE_KEEP_RECENT_TOKEN_BUDGET = 4_000;
|
|
@@ -81,6 +81,10 @@ export function isSafeCutPoint(messages: AgentMessage[], idx: number): boolean {
|
|
|
81
81
|
export function estimateMessageTokens(m: AgentMessage): number {
|
|
82
82
|
let chars = 0;
|
|
83
83
|
if (typeof m.content === "string") chars += m.content.length;
|
|
84
|
-
if (m.
|
|
84
|
+
if (m.role === "assistant" && m.tool_calls) {
|
|
85
|
+
for (const t of m.tool_calls) {
|
|
86
|
+
if (t.type === "function") chars += t.function.arguments.length;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
85
89
|
return Math.ceil(chars * APPROX_TOKENS_PER_CHAR) + 20;
|
|
86
90
|
}
|
|
@@ -28,6 +28,7 @@ import type { ToolCallView, ToolResultView } from "./hooks.js";
|
|
|
28
28
|
import { createToolHookResolver } from "./hooks.js";
|
|
29
29
|
import { loadGroupMaxVisible } from "./display-config.js";
|
|
30
30
|
import { classifySubmit, deriveChangeHandlerResult } from "./shell-mode.js";
|
|
31
|
+
import { UserShellIntents } from "./user-shell-intents.js";
|
|
31
32
|
|
|
32
33
|
const GROUPABLE_KINDS = new Set(["read", "search"]);
|
|
33
34
|
const TOOL_KIND: Record<string, string> = {
|
|
@@ -37,7 +38,7 @@ const TOOL_KIND: Record<string, string> = {
|
|
|
37
38
|
import { BusAutocompleteProvider } from "./autocomplete.js";
|
|
38
39
|
import { StatusFooter } from "./status-footer.js";
|
|
39
40
|
import type { MultiSessionStore } from "./multi-session-store.js";
|
|
40
|
-
import { stripContextWrappers, type SessionEntry } from "
|
|
41
|
+
import { stripContextWrappers, type SessionEntry } from "agent-sh/session-store";
|
|
41
42
|
import { formatSessionRow } from "./session-commands.js";
|
|
42
43
|
import { resumeSession } from "./session-commands.js";
|
|
43
44
|
import { applyBranchMessages } from "./commands.js";
|
|
@@ -300,8 +301,7 @@ export function mountAshi(
|
|
|
300
301
|
let processing = false;
|
|
301
302
|
const queuedQueries: string[] = [];
|
|
302
303
|
const queuedShellLines: { line: string; private: boolean }[] = [];
|
|
303
|
-
|
|
304
|
-
const pendingUserBlockPrivacy: boolean[] = [];
|
|
304
|
+
const pendingUserShell = new UserShellIntents();
|
|
305
305
|
|
|
306
306
|
const renderQueueSlot = (): void => {
|
|
307
307
|
queueSlot.clear();
|
|
@@ -325,7 +325,7 @@ export function mountAshi(
|
|
|
325
325
|
tui.requestRender();
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
328
|
-
|
|
328
|
+
pendingUserShell.push({ private: !!opts?.private });
|
|
329
329
|
if (opts?.private) bus.emit("shell:user-exec-exclude-next", {});
|
|
330
330
|
bus.emit("shell:pty-write", { data: line + "\n" });
|
|
331
331
|
};
|
|
@@ -442,21 +442,22 @@ export function mountAshi(
|
|
|
442
442
|
}
|
|
443
443
|
if (m.tool_calls) {
|
|
444
444
|
for (const tc of m.tool_calls) {
|
|
445
|
+
if (tc.type !== "function") continue;
|
|
445
446
|
const id = tc.id ?? "";
|
|
446
|
-
const name = tc.function
|
|
447
|
+
const name = tc.function.name ?? "tool";
|
|
447
448
|
const kind = TOOL_KIND[name];
|
|
448
449
|
if (kind && GROUPABLE_KINDS.has(kind)) {
|
|
449
450
|
const mergeable = findMergeableGroup(kind);
|
|
450
451
|
const group = mergeable
|
|
451
452
|
?? (() => { const g = new ToolGroup(kind, groupMaxVisible); chat.addChild(g); return g; })();
|
|
452
|
-
group.addCall(id, name, detailFromArgs(tc.function
|
|
453
|
+
group.addCall(id, name, detailFromArgs(tc.function.arguments));
|
|
453
454
|
if (id) toolMap.set(id, { kind: "group", group, name });
|
|
454
455
|
continue;
|
|
455
456
|
}
|
|
456
457
|
const pair = renderToolPair({
|
|
457
458
|
toolCallId: id, name, title: name, kind: undefined,
|
|
458
|
-
displayDetail: detailFromArgs(tc.function
|
|
459
|
-
rawInput: tc.function
|
|
459
|
+
displayDetail: detailFromArgs(tc.function.arguments),
|
|
460
|
+
rawInput: tc.function.arguments,
|
|
460
461
|
});
|
|
461
462
|
chat.addChild(pair.call);
|
|
462
463
|
chat.addChild(pair.result);
|
|
@@ -636,9 +637,11 @@ export function mountAshi(
|
|
|
636
637
|
let activeUserShell: { pair: ToolPair; command: string; isPrivate: boolean } | null = null;
|
|
637
638
|
bus.on("shell:command-start", ({ command }) => {
|
|
638
639
|
if (agentShellActive) return;
|
|
640
|
+
const intent = pendingUserShell.consume();
|
|
641
|
+
if (!intent) return;
|
|
639
642
|
finalizeThinking();
|
|
640
643
|
if (activeAssistant) { activeAssistant.finalize(); activeAssistant = null; }
|
|
641
|
-
const isPrivate =
|
|
644
|
+
const isPrivate = intent.private;
|
|
642
645
|
const name = isPrivate ? "user_bash_private" : "user_bash";
|
|
643
646
|
const pair = renderToolPair({
|
|
644
647
|
toolCallId: `user-shell-${Date.now()}`, name, title: name,
|
|
@@ -678,7 +681,7 @@ export function mountAshi(
|
|
|
678
681
|
// Shell queue drains first so its output lands in the next turn's <shell_events>.
|
|
679
682
|
while (queuedShellLines.length > 0) {
|
|
680
683
|
const item = queuedShellLines.shift()!;
|
|
681
|
-
|
|
684
|
+
pendingUserShell.push({ private: item.private });
|
|
682
685
|
if (item.private) bus.emit("shell:user-exec-exclude-next", {});
|
|
683
686
|
bus.emit("shell:pty-write", { data: item.line + "\n" });
|
|
684
687
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
|
-
import { SessionStore, type AgentMessage } from "
|
|
4
|
+
import { SessionStore, type AgentShMessage as AgentMessage } from "agent-sh/session-store";
|
|
5
5
|
|
|
6
6
|
export interface SessionInfo {
|
|
7
7
|
id: string;
|
|
@@ -55,13 +55,10 @@ export class MultiSessionStore {
|
|
|
55
55
|
for (const name of names) {
|
|
56
56
|
if (!name.endsWith(".jsonl")) continue;
|
|
57
57
|
const id = name.slice(0, -".jsonl".length);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
best = { id, createdAt: meta.createdAt };
|
|
63
|
-
}
|
|
64
|
-
} catch { /* skip unreadable meta */ }
|
|
58
|
+
const createdAt = readHeaderTimestamp(path.join(dir, name));
|
|
59
|
+
if (createdAt !== null && (!best || createdAt > best.createdAt)) {
|
|
60
|
+
best = { id, createdAt };
|
|
61
|
+
}
|
|
65
62
|
}
|
|
66
63
|
if (best) return best.id;
|
|
67
64
|
}
|
|
@@ -69,6 +66,21 @@ export class MultiSessionStore {
|
|
|
69
66
|
return undefined;
|
|
70
67
|
}
|
|
71
68
|
|
|
69
|
+
setName(id: string, name: string): void {
|
|
70
|
+
fs.writeFileSync(this.metaFile(id), JSON.stringify({ name }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private readName(id: string): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
const m = JSON.parse(fs.readFileSync(this.metaFile(id), "utf-8")) as { name?: string };
|
|
76
|
+
return typeof m.name === "string" ? m.name : undefined;
|
|
77
|
+
} catch { return undefined; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private metaFile(id: string): string {
|
|
81
|
+
return path.join(this.dir, `${id}.jsonl.meta`);
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
/** One-time import from the previous storage format (sessions stored as
|
|
73
85
|
* directories with tree.jsonl + snapshots/). Each old session is replayed
|
|
74
86
|
* from its most recent snapshot into a new flat `.jsonl` file, then the
|
|
@@ -140,12 +152,12 @@ export class MultiSessionStore {
|
|
|
140
152
|
const filePath = path.join(this.dir, name);
|
|
141
153
|
try {
|
|
142
154
|
const store = new SessionStore(filePath);
|
|
143
|
-
const
|
|
155
|
+
const root = store.getEntry(store.getRootId());
|
|
144
156
|
result.push({
|
|
145
157
|
id,
|
|
146
158
|
filePath,
|
|
147
|
-
createdAt:
|
|
148
|
-
name:
|
|
159
|
+
createdAt: root?.timestamp ?? 0,
|
|
160
|
+
name: this.readName(id),
|
|
149
161
|
preview: store.getPreview(),
|
|
150
162
|
entryCount: store.getAllEntries().length,
|
|
151
163
|
});
|
|
@@ -172,6 +184,17 @@ function newSessionFileId(): string {
|
|
|
172
184
|
return `${ts}_${suffix}`;
|
|
173
185
|
}
|
|
174
186
|
|
|
187
|
+
function readHeaderTimestamp(filePath: string): number | null {
|
|
188
|
+
try {
|
|
189
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
190
|
+
const nl = raw.indexOf("\n");
|
|
191
|
+
const firstLine = nl < 0 ? raw : raw.slice(0, nl);
|
|
192
|
+
const e = JSON.parse(firstLine) as { type?: string; timestamp?: number };
|
|
193
|
+
if (e.type === "session" && typeof e.timestamp === "number") return e.timestamp;
|
|
194
|
+
} catch { /* unreadable */ }
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
175
198
|
function writeImportedSession(
|
|
176
199
|
newFile: string,
|
|
177
200
|
id: string,
|
|
@@ -191,5 +214,5 @@ function writeImportedSession(
|
|
|
191
214
|
}
|
|
192
215
|
fs.writeFileSync(newFile, lines.join("\n") + "\n");
|
|
193
216
|
fs.writeFileSync(newFile + ".leaf", parent);
|
|
194
|
-
fs.writeFileSync(newFile + ".meta", JSON.stringify({
|
|
217
|
+
if (name) fs.writeFileSync(newFile + ".meta", JSON.stringify({ name }));
|
|
195
218
|
}
|
|
@@ -35,7 +35,7 @@ export function registerSessionCommands(
|
|
|
35
35
|
bus.emit("ui:error", { message: "name: expected a name" });
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
getStore().current().
|
|
38
|
+
getStore().setName(getStore().current().id, name);
|
|
39
39
|
bus.emit("ui:info", { message: `session named: ${name}` });
|
|
40
40
|
});
|
|
41
41
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Pending intents for ashi-issued shell pty-writes. shell:command-start fires
|
|
2
|
+
* for any OSC 9997 — orphans (bash DEBUG-trap noise) are dropped on consume. */
|
|
3
|
+
export interface UserShellIntent {
|
|
4
|
+
private: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class UserShellIntents {
|
|
8
|
+
private q: UserShellIntent[] = [];
|
|
9
|
+
|
|
10
|
+
push(intent: UserShellIntent): void {
|
|
11
|
+
this.q.push(intent);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
consume(): UserShellIntent | null {
|
|
15
|
+
return this.q.shift() ?? null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* `agent-sh auth login ollama-cloud` or OLLAMA_API_KEY). Local host
|
|
4
4
|
* overridable via OLLAMA_HOST.
|
|
5
5
|
*/
|
|
6
|
-
import { resolveApiKey } from "agent-sh/auth";
|
|
7
6
|
import type { AgentContext } from "agent-sh/types";
|
|
8
7
|
|
|
9
8
|
const ECHO_REASONING_PATTERNS: RegExp[] = [/deepseek/i];
|
|
@@ -14,7 +13,9 @@ function reasoningParams(level: string): Record<string, unknown> {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export default function activate(ctx: AgentContext): void {
|
|
17
|
-
const cloudKey =
|
|
16
|
+
const cloudKey =
|
|
17
|
+
(ctx.call("provider:resolve-api-key", "ollama-cloud") as { key: string | null }).key ??
|
|
18
|
+
process.env.OLLAMA_API_KEY;
|
|
18
19
|
const cloudHost = "https://ollama.com";
|
|
19
20
|
const cloudBaseURL = `${cloudHost}/v1`;
|
|
20
21
|
ctx.agent.providers.configure("ollama-cloud", { reasoningParams });
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import type { AgentContext } from "agent-sh/types";
|
|
34
|
-
import { resolveApiKey } from "agent-sh/auth";
|
|
35
34
|
|
|
36
35
|
// ── Constants ──────────────────────────────────────────────────────
|
|
37
36
|
|
|
@@ -180,7 +179,7 @@ function buildReasoningParams(level: string): Record<string, unknown> {
|
|
|
180
179
|
export default function activate(ctx: AgentContext): void {
|
|
181
180
|
const apiKey =
|
|
182
181
|
process.env.OPENCODE_API_KEY ??
|
|
183
|
-
|
|
182
|
+
(ctx.call("provider:resolve-api-key", "opencode") as { key: string | null }).key ?? undefined;
|
|
184
183
|
|
|
185
184
|
// ── Phase 1: register both providers synchronously with fallback models ──
|
|
186
185
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Auth: agent-sh auth login zai-coding-plan
|
|
5
5
|
* Usage: agent-sh -e ./examples/extensions/zai-coding-plan.ts
|
|
6
6
|
*/
|
|
7
|
-
import { resolveApiKey } from "agent-sh/auth";
|
|
8
7
|
import type { AgentContext } from "agent-sh/types";
|
|
9
8
|
|
|
10
9
|
const BASE_URL = "https://api.z.ai/api/coding/paas/v4";
|
|
@@ -24,7 +23,7 @@ function buildReasoningParams(level: string, _model?: string): Record<string, un
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export default function activate(ctx: AgentContext): void {
|
|
27
|
-
const { key } =
|
|
26
|
+
const { key } = ctx.call("provider:resolve-api-key", ID) as { key: string | null };
|
|
28
27
|
ctx.agent.providers.configure(ID, { reasoningParams: buildReasoningParams });
|
|
29
28
|
ctx.agent.providers.register({
|
|
30
29
|
id: ID,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -73,6 +73,18 @@
|
|
|
73
73
|
"types": "./dist/agent/types.d.ts",
|
|
74
74
|
"default": "./dist/agent/types.js"
|
|
75
75
|
},
|
|
76
|
+
"./store": {
|
|
77
|
+
"types": "./dist/agent/store.d.ts",
|
|
78
|
+
"default": "./dist/agent/store.js"
|
|
79
|
+
},
|
|
80
|
+
"./session-store": {
|
|
81
|
+
"types": "./dist/agent/session-store.d.ts",
|
|
82
|
+
"default": "./dist/agent/session-store.js"
|
|
83
|
+
},
|
|
84
|
+
"./entry-format": {
|
|
85
|
+
"types": "./dist/agent/entry-format.d.ts",
|
|
86
|
+
"default": "./dist/agent/entry-format.js"
|
|
87
|
+
},
|
|
76
88
|
"./agent/subagent": {
|
|
77
89
|
"types": "./dist/agent/subagent.d.ts",
|
|
78
90
|
"default": "./dist/agent/subagent.js"
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { ChatCompletionMessageParam } from "./llm-client.js";
|
|
2
|
-
import type { ImageContent } from "./types.js";
|
|
3
|
-
import { type NuclearEntry } from "./nuclear-form.js";
|
|
4
|
-
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
5
|
-
/** Search hit shape returned by the `history:search` handler. */
|
|
6
|
-
export interface HistoryHit {
|
|
7
|
-
entry: NuclearEntry;
|
|
8
|
-
line: string;
|
|
9
|
-
}
|
|
10
|
-
export interface CompactResult {
|
|
11
|
-
before: number;
|
|
12
|
-
after: number;
|
|
13
|
-
evictedCount: number;
|
|
14
|
-
[extra: string]: unknown;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Conversation state with eager nucleation — shell-history shaped.
|
|
18
|
-
*
|
|
19
|
-
* Every add nucleates into a one-line NuclearEntry and flushes to disk.
|
|
20
|
-
* Compaction evicts turns, replacing them with their nuclear one-liners
|
|
21
|
-
* in context; the originals stay searchable via `conversation_recall`
|
|
22
|
-
* and survive restarts in `~/.agent-sh/history`.
|
|
23
|
-
*
|
|
24
|
-
* Nucleation and history I/O go through advisable handlers — extensions
|
|
25
|
-
* swap strategies without touching this class. When no handlers are
|
|
26
|
-
* provided (subagents, tests), both become no-ops and this becomes a
|
|
27
|
-
* plain message buffer.
|
|
28
|
-
*/
|
|
29
|
-
export declare class ConversationState {
|
|
30
|
-
private messages;
|
|
31
|
-
private messagesDirty;
|
|
32
|
-
private cachedMessagesJson;
|
|
33
|
-
private toolErrors;
|
|
34
|
-
private nuclearEntries;
|
|
35
|
-
private nuclearBySeq;
|
|
36
|
-
private recallArchive;
|
|
37
|
-
readonly instanceId: string;
|
|
38
|
-
private readonly handlers;
|
|
39
|
-
private nextSeq;
|
|
40
|
-
private lastApiTokenCount;
|
|
41
|
-
private lastApiMessageCount;
|
|
42
|
-
private pendingMessages;
|
|
43
|
-
constructor(handlers?: HandlerFunctions, instanceId?: string);
|
|
44
|
-
/** Get JSON.stringify of messages, cached until next mutation. */
|
|
45
|
-
private getMessagesJson;
|
|
46
|
-
private invalidateMessagesCache;
|
|
47
|
-
addUserMessage(text: string): void;
|
|
48
|
-
addAssistantMessage(content: string | null, toolCalls?: {
|
|
49
|
-
id: string;
|
|
50
|
-
function: {
|
|
51
|
-
name: string;
|
|
52
|
-
arguments: string;
|
|
53
|
-
};
|
|
54
|
-
}[], extras?: Record<string, unknown>): void;
|
|
55
|
-
addToolResult(toolCallId: string, content: string | ImageContent[], isError?: boolean): void;
|
|
56
|
-
/** Add tool results as a user message (for inline tool protocol). */
|
|
57
|
-
addToolResultInline(content: string): void;
|
|
58
|
-
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
59
|
-
addSystemNote(text: string): void;
|
|
60
|
-
appendUserMessage(text: string): void;
|
|
61
|
-
private hasOpenToolCalls;
|
|
62
|
-
private flushPendingMessages;
|
|
63
|
-
getMessages(): ChatCompletionMessageParam[];
|
|
64
|
-
/** Drop tool messages with no matching preceding tool_call — strict
|
|
65
|
-
* providers (DeepSeek) 400, and compaction can leave such orphans. */
|
|
66
|
-
private dropOrphanToolMessages;
|
|
67
|
-
/**
|
|
68
|
-
* If a stream was interrupted mid-tool-execution, an assistant message
|
|
69
|
-
* with tool_calls can land in history without matching tool results.
|
|
70
|
-
* Strict providers (DeepSeek) 400 on this. Stub each missing result
|
|
71
|
-
* with a [cancelled] marker so the protocol stays valid.
|
|
72
|
-
*/
|
|
73
|
-
private stubDanglingToolCalls;
|
|
74
|
-
/**
|
|
75
|
-
* DeepSeek 400s if any assistant in a thinking-mode conversation is
|
|
76
|
-
* missing reasoning_content. Cross-alias here (OpenRouter streams as
|
|
77
|
-
* `reasoning`, DeepSeek input expects `reasoning_content`) and stub
|
|
78
|
-
* gaps (text-only turns, pre-fix messages) with empty string.
|
|
79
|
-
*/
|
|
80
|
-
private normalizeReasoningConsistency;
|
|
81
|
-
/**
|
|
82
|
-
* Replace the messages array wholesale — the write side for custom
|
|
83
|
-
* compaction strategies. Invalidates API token baseline since the
|
|
84
|
-
* new array's token count is unknown.
|
|
85
|
-
*/
|
|
86
|
-
replaceMessages(messages: ChatCompletionMessageParam[]): void;
|
|
87
|
-
private pruneToolErrors;
|
|
88
|
-
private eagerNucleateUser;
|
|
89
|
-
/** Nucleate an agent text response. Called by agent-loop when the loop finishes without tool calls. */
|
|
90
|
-
eagerNucleateAgent(text: string): void;
|
|
91
|
-
/** Nucleate tool call results. One entry per tool call, enriched with result. */
|
|
92
|
-
eagerNucleateTools(results: Array<{
|
|
93
|
-
toolName: string;
|
|
94
|
-
args: Record<string, unknown>;
|
|
95
|
-
content: string | ImageContent[];
|
|
96
|
-
isError: boolean;
|
|
97
|
-
}>): void;
|
|
98
|
-
/** Track an entry in memory (nuclear list + recall archive). */
|
|
99
|
-
private recordNuclearEntry;
|
|
100
|
-
private appendToHistory;
|
|
101
|
-
/** Bump and return the global sequence counter. For extensions that
|
|
102
|
-
* synthesize their own NuclearEntries (e.g. compaction summaries that
|
|
103
|
-
* should land in the same sequence space as kernel-produced entries). */
|
|
104
|
-
allocateSeq(): number;
|
|
105
|
-
/** Clear nuclear bookkeeping and reset the seq counter. For extensions
|
|
106
|
-
* that swap sessions (multi-session history adapters) so the in-memory
|
|
107
|
-
* nuclear list, recall archive, and seq counter don't carry over from
|
|
108
|
-
* the previous session's tree. */
|
|
109
|
-
resetForSession(nextSeq: number): void;
|
|
110
|
-
updateApiTokenCount(promptTokens: number): void;
|
|
111
|
-
estimatePromptTokens(): number;
|
|
112
|
-
estimateTokens(): number;
|
|
113
|
-
/**
|
|
114
|
-
* Two-tier pin compaction: evict lowest-priority turns (replaced by
|
|
115
|
-
* their nuclear one-liners), slim the window before the last verbatim
|
|
116
|
-
* turn, drop read-only tool results entirely. Extensions replace the
|
|
117
|
-
* whole strategy by advising `conversation:compact` and skipping next.
|
|
118
|
-
*/
|
|
119
|
-
compact(maxPromptTokens: number, recentTurnsToKeep?: number, force?: boolean): CompactResult | null;
|
|
120
|
-
/**
|
|
121
|
-
* Inject prior session history as a context preamble. The preamble
|
|
122
|
-
* layout goes through the `conversation:format-prior-history` handler,
|
|
123
|
-
* so extensions can swap the flat list for grouped/richer rendering.
|
|
124
|
-
*/
|
|
125
|
-
loadPriorHistory(entries: NuclearEntry[]): void;
|
|
126
|
-
search(query: string): Promise<string>;
|
|
127
|
-
expand(seq: number): Promise<string>;
|
|
128
|
-
browse(): Promise<string>;
|
|
129
|
-
getNuclearEntries(): readonly NuclearEntry[];
|
|
130
|
-
getNuclearEntryCount(): number;
|
|
131
|
-
getNuclearSummary(): string | null;
|
|
132
|
-
getRecallArchiveSize(): number;
|
|
133
|
-
clear(): void;
|
|
134
|
-
private buildNuclearBlock;
|
|
135
|
-
/** Index of the nuclear block in messages[], or -1 if not present. */
|
|
136
|
-
private nuclearBlockIdx;
|
|
137
|
-
private updateNuclearBlockInMessages;
|
|
138
|
-
private slimTurn;
|
|
139
|
-
private parseTurns;
|
|
140
|
-
private inferPriority;
|
|
141
|
-
private turnToText;
|
|
142
|
-
}
|