agent-sh 0.14.8 → 0.14.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 +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 +50 -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/providers/ollama.d.ts +11 -0
- package/dist/agent/providers/ollama.js +72 -0
- package/dist/agent/providers/opencode.d.ts +10 -0
- package/dist/agent/providers/opencode.js +112 -0
- package/dist/agent/providers/zai-coding-plan.d.ts +5 -0
- package/dist/agent/providers/zai-coding-plan.js +26 -0
- 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/args.js +2 -2
- 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/tui-renderer.js +5 -2
- package/dist/utils/diff-renderer.js +9 -7
- package/examples/extensions/ads/index.ts +695 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
- package/examples/extensions/ash-scheme/index.ts +77 -3
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/capture.ts +1 -1
- package/examples/extensions/ashi/src/cli.ts +5 -6
- package/examples/extensions/ashi/src/compaction.ts +6 -2
- package/examples/extensions/ashi/src/frontend.ts +13 -13
- 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/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
|
@@ -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
|
|
|
@@ -319,6 +319,36 @@ function format(v: unknown): string {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// ── evaluator ─────────────────────────────────────────────────────
|
|
322
|
+
// LIPS 0.20.x has no character type, so a `#\…` literal reads as an unbound
|
|
323
|
+
// symbol. preprocessSchemeSource expands each to a 1-char string instead.
|
|
324
|
+
const NAMED_CHARS: Record<string, string> = {
|
|
325
|
+
newline: "\n", linefeed: "\n", nl: "\n",
|
|
326
|
+
space: " ", tab: "\t", return: "\r",
|
|
327
|
+
null: "\u0000", nul: "\u0000",
|
|
328
|
+
delete: "\u007f", rubout: "\u007f",
|
|
329
|
+
escape: "\u001b", altmode: "\u001b",
|
|
330
|
+
backspace: "\u0008", alarm: "\u0007", page: "\u000c",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// The char after `#\` as a 1-char string + its span, or null for an unknown
|
|
334
|
+
// multi-char name (left untranslated so it surfaces as an error).
|
|
335
|
+
function resolveCharLiteral(source: string, start: number): { ch: string; span: number } | null {
|
|
336
|
+
let j = start;
|
|
337
|
+
while (j < source.length && /[A-Za-z0-9]/.test(source[j])) j++;
|
|
338
|
+
const run = source.slice(start, j);
|
|
339
|
+
if (run.length <= 1) {
|
|
340
|
+
const ch = source[start];
|
|
341
|
+
return ch === undefined ? null : { ch, span: 1 };
|
|
342
|
+
}
|
|
343
|
+
const hex = /^[xX]([0-9a-fA-F]+)$/.exec(run);
|
|
344
|
+
if (hex) {
|
|
345
|
+
const cp = parseInt(hex[1], 16);
|
|
346
|
+
if (cp >= 0 && cp <= 0x10ffff) return { ch: String.fromCodePoint(cp), span: run.length };
|
|
347
|
+
}
|
|
348
|
+
const named = NAMED_CHARS[run.toLowerCase()];
|
|
349
|
+
return named === undefined ? null : { ch: named, span: run.length };
|
|
350
|
+
}
|
|
351
|
+
|
|
322
352
|
// LIPS implements string literals via JSON.parse, which rejects backslash
|
|
323
353
|
// escapes outside JSON's tiny set (\" \\ \/ \b \f \n \r \t \uXXXX). Models
|
|
324
354
|
// routinely write \s \w \d etc. in regex strings. Pre-process: promote any
|
|
@@ -339,6 +369,14 @@ function preprocessSchemeSource(source: string): string {
|
|
|
339
369
|
if (!inStr) {
|
|
340
370
|
if (c === ";") { inComment = true; out += c; continue; }
|
|
341
371
|
if (c === '"') { inStr = true; out += c; continue; }
|
|
372
|
+
if (c === "#" && source[i + 1] === "\\") {
|
|
373
|
+
const lit = resolveCharLiteral(source, i + 2);
|
|
374
|
+
if (lit) {
|
|
375
|
+
out += JSON.stringify(lit.ch);
|
|
376
|
+
i += 1 + lit.span; // skip '\' + the name/char; loop's i++ skips '#'
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
342
380
|
out += c;
|
|
343
381
|
continue;
|
|
344
382
|
}
|
|
@@ -518,6 +556,11 @@ async function evaluate(env: any, source: string, timeoutMs: number) {
|
|
|
518
556
|
msg = formatParenDiagnostic(source, msg);
|
|
519
557
|
} else if (/Bad escaped character in JSON|Unexpected.*JSON|JSON at position/.test(msg)) {
|
|
520
558
|
msg = formatStringEscapeDiagnostic(source, msg);
|
|
559
|
+
} else if (msg.includes("Unbound variable `#\\")) {
|
|
560
|
+
msg += "\n This runtime has no character type — characters are 1-char strings." +
|
|
561
|
+
" `#\\newline`, `#\\space`, `#\\tab`, `#\\return`, `#\\xNN`, and `#\\<char>`" +
|
|
562
|
+
" are accepted (read as strings); other `#\\<name>` forms are not." +
|
|
563
|
+
" Use a string literal instead, e.g. \"\\n\".";
|
|
521
564
|
}
|
|
522
565
|
return { ok: false as const, error: msg };
|
|
523
566
|
} finally {
|
|
@@ -1370,6 +1413,25 @@ function installStdShims(env: any): void {
|
|
|
1370
1413
|
});
|
|
1371
1414
|
defineIfMissing("char-upcase", (c: any) => String(c).toUpperCase());
|
|
1372
1415
|
defineIfMissing("char-downcase", (c: any) => String(c).toLowerCase());
|
|
1416
|
+
// Characters are 1-char strings; char? therefore can't tell a char from a
|
|
1417
|
+
// length-1 string.
|
|
1418
|
+
const charStr = (c: any) => (typeof c === "string" ? c : String(c));
|
|
1419
|
+
const codeOf = (c: any) => charStr(c).codePointAt(0) ?? 0;
|
|
1420
|
+
defineIfMissing("char->integer", (c: any) => codeOf(c));
|
|
1421
|
+
defineIfMissing("integer->char", (n: any) => String.fromCodePoint(Math.max(0, Math.floor(Number(n) || 0))));
|
|
1422
|
+
defineIfMissing("char?", (x: any) => typeof x === "string" && Array.from(x).length === 1);
|
|
1423
|
+
defineIfMissing("char=?", (a: any, b: any) => charStr(a) === charStr(b));
|
|
1424
|
+
defineIfMissing("char<?", (a: any, b: any) => codeOf(a) < codeOf(b));
|
|
1425
|
+
defineIfMissing("char>?", (a: any, b: any) => codeOf(a) > codeOf(b));
|
|
1426
|
+
defineIfMissing("char<=?", (a: any, b: any) => codeOf(a) <= codeOf(b));
|
|
1427
|
+
defineIfMissing("char>=?", (a: any, b: any) => codeOf(a) >= codeOf(b));
|
|
1428
|
+
defineIfMissing("char-ci=?", (a: any, b: any) => charStr(a).toLowerCase() === charStr(b).toLowerCase());
|
|
1429
|
+
defineIfMissing("char-alphabetic?", (c: any) => /^\p{L}$/u.test(charStr(c)));
|
|
1430
|
+
defineIfMissing("char-numeric?", (c: any) => /^\p{Nd}$/u.test(charStr(c)));
|
|
1431
|
+
defineIfMissing("char-whitespace?", (c: any) => /^\s$/.test(charStr(c)));
|
|
1432
|
+
defineIfMissing("char-upper-case?", (c: any) => { const s = charStr(c); return s.length === 1 && s === s.toUpperCase() && s !== s.toLowerCase(); });
|
|
1433
|
+
defineIfMissing("char-lower-case?", (c: any) => { const s = charStr(c); return s.length === 1 && s === s.toLowerCase() && s !== s.toUpperCase(); });
|
|
1434
|
+
defineIfMissing("digit-value", (c: any) => { const s = charStr(c); return /^[0-9]$/.test(s) ? Number(s) : false; });
|
|
1373
1435
|
defineIfMissing("string-normalize-spaces", (s: any, sep?: any, repl?: any) => {
|
|
1374
1436
|
const str = String(s).trim();
|
|
1375
1437
|
const splitOn = sep === undefined ? /\s+/ : (sep instanceof RegExp ? sep : new RegExp(String(sep)));
|
|
@@ -1500,9 +1562,9 @@ function installStdShims(env: any): void {
|
|
|
1500
1562
|
}
|
|
1501
1563
|
|
|
1502
1564
|
// Canonical names we claim coverage for. R = R7RS small base; S = SRFI-1;
|
|
1503
|
-
// K = Racket racket/base/list/string/format. Continuations, ports,
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1565
|
+
// K = Racket racket/base/list/string/format. Continuations, ports, and
|
|
1566
|
+
// bytevectors are intentionally omitted — they'd be misleading "coverage"
|
|
1567
|
+
// without real functionality. Characters are modeled as 1-char strings.
|
|
1506
1568
|
const COVERAGE_CHECKLIST: string[] = [
|
|
1507
1569
|
// R7RS § 6.1 equivalence
|
|
1508
1570
|
"eq?", "eqv?", "equal?",
|
|
@@ -1584,6 +1646,10 @@ const COVERAGE_CHECKLIST: string[] = [
|
|
|
1584
1646
|
// Racket strings & chars (gaps)
|
|
1585
1647
|
"string-titlecase", "string-pad", "string-pad-right",
|
|
1586
1648
|
"char-upcase", "char-downcase",
|
|
1649
|
+
"char->integer", "integer->char", "char?",
|
|
1650
|
+
"char=?", "char<?", "char>?", "char<=?", "char>=?", "char-ci=?",
|
|
1651
|
+
"char-alphabetic?", "char-numeric?", "char-whitespace?",
|
|
1652
|
+
"char-upper-case?", "char-lower-case?", "digit-value",
|
|
1587
1653
|
"string-normalize-spaces", "build-string",
|
|
1588
1654
|
// Racket hash
|
|
1589
1655
|
"make-hash", "hash", "hash?", "hash-ref", "hash-set!", "hash-set",
|
|
@@ -1841,6 +1907,10 @@ function installBindings(
|
|
|
1841
1907
|
env.set("string-contains", stringContains);
|
|
1842
1908
|
env.set("string-append", (...parts: unknown[]) =>
|
|
1843
1909
|
parts.map((p) => (p === undefined || p === null ? "" : String(p))).join(""));
|
|
1910
|
+
// LIPS' native `string` returns only its first argument; override to actually
|
|
1911
|
+
// concatenate (chars are 1-char strings here, so that's the right semantics).
|
|
1912
|
+
env.set("string", (...parts: unknown[]) =>
|
|
1913
|
+
parts.map((p) => (p === undefined || p === null ? "" : String(p))).join(""));
|
|
1844
1914
|
env.set("number->string", (n: unknown) => String(n));
|
|
1845
1915
|
env.set("string->number", (s: unknown) => {
|
|
1846
1916
|
if (typeof s !== "string") return false;
|
|
@@ -1949,6 +2019,10 @@ const DESCRIPTION = [
|
|
|
1949
2019
|
" - R7RS truthy semantics: anything that isn't `#f` is true. `(if str …)`,",
|
|
1950
2020
|
" `(if 0 …)`, `(if '() …)` all take the then-branch.",
|
|
1951
2021
|
" - `#t`/`#f` work as expected. `equal?`, `eq?`, `eqv?`, `string=?` all work.",
|
|
2022
|
+
" - Characters are 1-char strings (no separate char type). `#\\newline`,",
|
|
2023
|
+
" `#\\space`, `#\\tab`, `#\\return`, `#\\xNN`, and `#\\<char>` literals read as",
|
|
2024
|
+
" the equivalent string; `char->integer`/`integer->char`/`char?`/`char=?`/",
|
|
2025
|
+
" `char-whitespace?` etc. operate on them. A bare newline is just \"\\n\".",
|
|
1952
2026
|
" - SRFI-1: `member`, `assq`/`assv`/`assoc`, `delete-duplicates`, `first`",
|
|
1953
2027
|
" through `fifth`, `last`, `take`, `drop`, `iota`, `any`, `every`, `count`,",
|
|
1954
2028
|
" `find`, `filter-map`, `append-map`, `concatenate`, `partition`, `remove`,",
|
|
@@ -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";
|
|
@@ -36,8 +36,8 @@ import * as path from "node:path";
|
|
|
36
36
|
|
|
37
37
|
function parseArgs(argv: string[]): AppConfig & { extensions?: string[]; continueLast: boolean } {
|
|
38
38
|
let model: string | undefined;
|
|
39
|
-
let apiKey: string | undefined
|
|
40
|
-
let baseURL: string | undefined
|
|
39
|
+
let apiKey: string | undefined;
|
|
40
|
+
let baseURL: string | undefined;
|
|
41
41
|
let provider: string | undefined;
|
|
42
42
|
let backend: string | undefined;
|
|
43
43
|
let continueLast = false;
|
|
@@ -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,12 +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;
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (!command.trim()) return;
|
|
640
|
+
const intent = pendingUserShell.consume();
|
|
641
|
+
if (!intent) return;
|
|
642
642
|
finalizeThinking();
|
|
643
643
|
if (activeAssistant) { activeAssistant.finalize(); activeAssistant = null; }
|
|
644
|
-
const isPrivate =
|
|
644
|
+
const isPrivate = intent.private;
|
|
645
645
|
const name = isPrivate ? "user_bash_private" : "user_bash";
|
|
646
646
|
const pair = renderToolPair({
|
|
647
647
|
toolCallId: `user-shell-${Date.now()}`, name, title: name,
|
|
@@ -681,7 +681,7 @@ export function mountAshi(
|
|
|
681
681
|
// Shell queue drains first so its output lands in the next turn's <shell_events>.
|
|
682
682
|
while (queuedShellLines.length > 0) {
|
|
683
683
|
const item = queuedShellLines.shift()!;
|
|
684
|
-
|
|
684
|
+
pendingUserShell.push({ private: item.private });
|
|
685
685
|
if (item.private) bus.emit("shell:user-exec-exclude-next", {});
|
|
686
686
|
bus.emit("shell:pty-write", { data: item.line + "\n" });
|
|
687
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.10",
|
|
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
|
-
}
|