agent.libx.js 0.92.8 → 0.93.1
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/cli/cli.ts +147 -18
- package/dist/{Agent-QwBA0wu6.d.ts → Agent-B_xvSHlG.d.ts} +7 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +414 -130
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -22
- package/dist/index.js +207 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/cli/cli.ts
CHANGED
|
@@ -238,7 +238,7 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
|
|
|
238
238
|
Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
|
|
239
239
|
|
|
240
240
|
REPL shortcuts: !<cmd> runs a shell command inline · #<note> saves a memory · @path inlines a file
|
|
241
|
-
REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /exit
|
|
241
|
+
REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit
|
|
242
242
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu — ↑/↓ select, ⏎/Tab accept, Esc dismiss.
|
|
243
243
|
REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
|
|
244
244
|
REPL shortcuts: Shift+Tab cycles permission posture (ask → accept-edits → plan) · Alt+T toggles reasoning · Alt+P switches model · Ctrl+O toggles verbose tool output · → or Tab accepts the dim history ghost-suggestion · Alt+S/Ctrl+S stash/unstash.
|
|
@@ -504,6 +504,31 @@ function turnCost(model: string, usage?: { promptTokens?: number; completionToke
|
|
|
504
504
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0);
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
/** Evaluate whether a goal condition has been met, based on recent transcript. */
|
|
508
|
+
async function evaluateGoal(ai: ChatLike, condition: string, transcript: Message[], log: (s: string) => void): Promise<{ met: boolean; reason: string }> {
|
|
509
|
+
const recent = transcript
|
|
510
|
+
.filter((m) => m.role === 'assistant')
|
|
511
|
+
.slice(-8)
|
|
512
|
+
.map((m) => {
|
|
513
|
+
const text = typeof m.content === 'string' ? m.content : (m.content as ContentPart[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join(' ');
|
|
514
|
+
return text.slice(0, 600);
|
|
515
|
+
})
|
|
516
|
+
.join('\n---\n');
|
|
517
|
+
try {
|
|
518
|
+
const r = await ai.chat({
|
|
519
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
520
|
+
stream: false,
|
|
521
|
+
messages: [
|
|
522
|
+
{ role: 'system', content: 'You judge whether a goal condition has been met based on conversation transcript. Respond ONLY with JSON: {"met": boolean, "reason": "one sentence"}. Be strict — only met:true if there is clear evidence in the transcript.' },
|
|
523
|
+
{ role: 'user', content: `Goal condition: ${condition}\n\nRecent assistant messages:\n${recent}` },
|
|
524
|
+
],
|
|
525
|
+
}) as { content: string };
|
|
526
|
+
const match = r.content.match(/\{[\s\S]*\}/);
|
|
527
|
+
if (match) return JSON.parse(match[0]);
|
|
528
|
+
} catch (e: any) { log(dim(` (goal evaluator error: ${e?.message ?? e})\n`)); }
|
|
529
|
+
return { met: false, reason: 'evaluation unclear' };
|
|
530
|
+
}
|
|
531
|
+
|
|
507
532
|
/** Normalize a thrown/returned error into the persisted forensic shape. */
|
|
508
533
|
function errInfo(e: any): { message: string; statusCode?: number; code?: string } {
|
|
509
534
|
return { message: String(e?.message ?? e), statusCode: e?.statusCode, code: e?.code };
|
|
@@ -547,6 +572,11 @@ export async function runShellLine(fs: IFilesystem, cmd: string): Promise<string
|
|
|
547
572
|
return out || '(no output)';
|
|
548
573
|
}
|
|
549
574
|
|
|
575
|
+
/** Resolve a memoryDir (string or string[]) to the primary (write) dir. */
|
|
576
|
+
function primaryMemDir(dir: string | string[] | undefined, fallback: string): string {
|
|
577
|
+
return (Array.isArray(dir) ? dir[0] : dir) || fallback;
|
|
578
|
+
}
|
|
579
|
+
|
|
550
580
|
/** Append a `#note` to the memory index (creating the dir/file if needed). Returns the file path. */
|
|
551
581
|
export async function appendMemoryNote(fs: IFilesystem, dir: string, text: string): Promise<string> {
|
|
552
582
|
await mkdirp(fs, dir);
|
|
@@ -990,14 +1020,21 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
990
1020
|
const duplexAsk = async (call: ToolUse): Promise<{ decision: 'allow' | 'deny' }> => {
|
|
991
1021
|
if (args.voice && dx) {
|
|
992
1022
|
const hint = summarizeCall(call.name, call.args).slice(0, 80);
|
|
1023
|
+
// 'menu' mode: approve like a normal session — suspend the editor, pop the picker.
|
|
1024
|
+
if ((cfg as any).voiceAskUi === 'menu') {
|
|
1025
|
+
editorRef?.suspend();
|
|
1026
|
+
const v = await selectMenu(process.stderr, { title: `? background worker asks to run ${call.name} ${hint}`, items: [{ label: 'Allow', value: 'y' }, { label: 'Deny', value: 'n' }], current: 'n' });
|
|
1027
|
+
editorRef?.resume();
|
|
1028
|
+
editorRef?.redrawNow();
|
|
1029
|
+
return { decision: v === 'y' ? 'allow' : 'deny' };
|
|
1030
|
+
}
|
|
993
1031
|
// NB: perm asks are keyed perm-N (PermissionPolicy.ask carries no task identity), so a
|
|
994
1032
|
// cancelled task can't clean its parked perm question — bounded by askTimeoutMs → deny.
|
|
1033
|
+
// (Chrome prints once via the task_ask notify below — no extra line here.)
|
|
995
1034
|
const id = `perm-${++permSeq}`;
|
|
996
|
-
|
|
997
|
-
editorRef?.redrawNow();
|
|
998
|
-
const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ''}? Yes or no.`);
|
|
1035
|
+
const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ''}? Answer yes or no (you can also type it).`);
|
|
999
1036
|
const allow = /^\s*(y(es|ep|eah)?|sure|ok(ay)?|allow|go|approved?|do it)\b/i.test(a);
|
|
1000
|
-
err('\r\x1b[0J' +
|
|
1037
|
+
err('\r\x1b[0J' + (allow ? green(` ✓ allowed ${call.name}`) : yellow(` ⊘ denied ${call.name}`)) + dim(` (${a.trim() || 'no answer'})\n`));
|
|
1001
1038
|
editorRef?.redrawNow();
|
|
1002
1039
|
return { decision: allow ? 'allow' : 'deny' };
|
|
1003
1040
|
}
|
|
@@ -1060,7 +1097,8 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1060
1097
|
// residue glued to 'agentx › '). task_done returned above; everything else falls through.
|
|
1061
1098
|
if (typeof e.kind === 'string' && e.kind.startsWith('task_')) {
|
|
1062
1099
|
spinner.stop();
|
|
1063
|
-
|
|
1100
|
+
// asks are decisions, not chatter — make them stand out (the voice also speaks them)
|
|
1101
|
+
err('\r\x1b[0J' + (e.kind === 'task_ask' ? yellow(` ? ${e.message} — answer by voice or type yes/no\n`) : dim(` · ${e.message}\n`)));
|
|
1064
1102
|
editorRef?.redrawNow();
|
|
1065
1103
|
return;
|
|
1066
1104
|
}
|
|
@@ -1084,6 +1122,8 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1084
1122
|
dx = new DuplexAgent({
|
|
1085
1123
|
ai,
|
|
1086
1124
|
fs: agent.options.fs,
|
|
1125
|
+
memoryDir: agent.options.memoryDir,
|
|
1126
|
+
memoryUserDir: agent.options.memoryUserDir,
|
|
1087
1127
|
...((args.voiceModel ?? cfg.voiceModel) ? { voiceModel: resolveModelOrNewest((args.voiceModel ?? cfg.voiceModel)!) } : {}),
|
|
1088
1128
|
workerModel: agent.options.model,
|
|
1089
1129
|
workerOptions,
|
|
@@ -1101,14 +1141,14 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1101
1141
|
return head.startsWith('ref: refs/heads/') ? `branch: ${head.slice('ref: refs/heads/'.length)}` : `detached HEAD at ${head.slice(0, 12)}`;
|
|
1102
1142
|
} catch { return 'not a git repository'; }
|
|
1103
1143
|
},
|
|
1104
|
-
// Memory READS are QuickLook material (instant, capped); memory WRITES stay delegated —
|
|
1105
|
-
// a worker creates/updates the files under .agent/memory/.
|
|
1106
1144
|
memory: async () => {
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1145
|
+
const _adot = (s: string) => `${(agent.options.fs!.getCwd() === '/' ? '' : agent.options.fs!.getCwd())}/.agent/${s}`;
|
|
1146
|
+
const dirs = Array.isArray(agent.options.memoryDir) ? agent.options.memoryDir : [agent.options.memoryDir || _adot('memory')];
|
|
1147
|
+
const parts: string[] = [];
|
|
1148
|
+
for (const d of dirs) {
|
|
1149
|
+
try { const idx = await fs.readFile(`${d}/MEMORY.md`); if (idx.trim()) parts.push(idx.trim()); } catch { /* dir doesn't exist yet */ }
|
|
1150
|
+
}
|
|
1151
|
+
return parts.length ? parts.join('\n').slice(0, 2000) : '(no memory yet)';
|
|
1112
1152
|
},
|
|
1113
1153
|
},
|
|
1114
1154
|
// The voice runs on the REAL fs (it has no fs tools — harmless) so @mentions, !cmd and #note
|
|
@@ -1230,7 +1270,14 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1230
1270
|
const s = Math.max(0, (Date.now() - t) / 1000);
|
|
1231
1271
|
return s < 60 ? 'just now' : s < 3600 ? `${Math.floor(s / 60)}m ago` : s < 86400 ? `${Math.floor(s / 3600)}h ago` : `${Math.floor(s / 86400)}d ago`;
|
|
1232
1272
|
};
|
|
1233
|
-
const resumeInto = (data: SessionData) => {
|
|
1273
|
+
const resumeInto = (data: SessionData) => {
|
|
1274
|
+
face.transcript = data.messages; session = data; checkpoints.use?.(data.meta.id);
|
|
1275
|
+
const m = data.meta as any;
|
|
1276
|
+
goalCondition = m.goalCondition; goalTurns = m.goalTurns ?? 0; goalTokens = m.goalTokens ?? 0; goalLastReason = m.goalLastReason;
|
|
1277
|
+
err(dim(` resumed ${data.meta.id} (${data.meta.turns} turns)${data.meta.title ? ' — ' + data.meta.title : ''}\n`));
|
|
1278
|
+
if (goalCondition) err(dim(` ◎ goal active: ${goalCondition} (${goalTurns} turns)\n`));
|
|
1279
|
+
printHistory(data.messages);
|
|
1280
|
+
};
|
|
1234
1281
|
// Double-Esc rewind (CC parity): pick an earlier user message, then choose what to restore —
|
|
1235
1282
|
// conversation, code, or both. Returns the message text to pre-fill the prompt (when the conversation
|
|
1236
1283
|
// is rewound) for editing + resend, or undefined (cancelled, or code-only).
|
|
@@ -1357,6 +1404,41 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1357
1404
|
return picked && !picked.startsWith('__') ? picked : null;
|
|
1358
1405
|
};
|
|
1359
1406
|
|
|
1407
|
+
let goalCondition: string | undefined = (session.meta as any).goalCondition;
|
|
1408
|
+
let goalTurns: number = (session.meta as any).goalTurns ?? 0;
|
|
1409
|
+
let goalTokens: number = (session.meta as any).goalTokens ?? 0;
|
|
1410
|
+
let goalLastReason: string | undefined = (session.meta as any).goalLastReason;
|
|
1411
|
+
const GOAL_MAX_TURNS = 50;
|
|
1412
|
+
const persistGoal = () => {
|
|
1413
|
+
const m = session.meta as any;
|
|
1414
|
+
m.goalCondition = goalCondition; m.goalTurns = goalTurns; m.goalTokens = goalTokens; m.goalLastReason = goalLastReason;
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
// ---- /goal: autonomous loop with a halting condition ----
|
|
1418
|
+
const goalLoop = async () => {
|
|
1419
|
+
while (goalCondition && !aborting && !exitRequested && goalTurns < GOAL_MAX_TURNS) {
|
|
1420
|
+
const result = await evaluateGoal(ai, goalCondition, face.transcript, err);
|
|
1421
|
+
goalLastReason = result.reason;
|
|
1422
|
+
if (result.met) {
|
|
1423
|
+
err(green(` ✓ goal met: ${result.reason}\n`));
|
|
1424
|
+
goalCondition = undefined; goalLastReason = undefined;
|
|
1425
|
+
persistGoal();
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
err(dim(` ◎ not yet (${result.reason}) — turn ${goalTurns + 1}\n`));
|
|
1429
|
+
aborting = false;
|
|
1430
|
+
const tokensBefore = session.meta.tokens ?? 0;
|
|
1431
|
+
await turn(`Continue working toward the goal: ${goalCondition}`);
|
|
1432
|
+
goalTokens += (session.meta.tokens ?? 0) - tokensBefore;
|
|
1433
|
+
goalTurns++;
|
|
1434
|
+
persistGoal();
|
|
1435
|
+
if (exitRequested) return;
|
|
1436
|
+
}
|
|
1437
|
+
if (goalTurns >= GOAL_MAX_TURNS) {
|
|
1438
|
+
err(yellow(` ⚠ goal reached ${GOAL_MAX_TURNS} turns — pausing. /goal to check status, /goal clear to cancel\n`));
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1360
1442
|
// ---- slash builtins: name → handler. Returns true to exit the REPL. ----
|
|
1361
1443
|
const builtins: Record<string, { desc: string; run: (a: string[]) => boolean | void | Promise<boolean | void> }> = {
|
|
1362
1444
|
help: { desc: 'show this help', run: () => { err(HELP + '\n'); } },
|
|
@@ -1696,6 +1778,35 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1696
1778
|
} catch (e: any) { err(red(` export failed: ${e?.message ?? e}\n`)); }
|
|
1697
1779
|
},
|
|
1698
1780
|
},
|
|
1781
|
+
goal: {
|
|
1782
|
+
desc: 'autonomous loop — /goal <condition> | /goal (status) | /goal clear',
|
|
1783
|
+
run: async (a) => {
|
|
1784
|
+
if (!a.length) {
|
|
1785
|
+
if (!goalCondition) { err(dim(' no active goal\n')); return; }
|
|
1786
|
+
const tokStr = goalTokens > 1000 ? `${(goalTokens / 1000).toFixed(1)}k` : String(goalTokens);
|
|
1787
|
+
err(` ${bold('◎ goal:')} ${goalCondition}\n` + dim(` ${goalTurns} turn${goalTurns === 1 ? '' : 's'} · ${tokStr} tokens${goalLastReason ? ` · last: ${goalLastReason}` : ''}\n`));
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (a[0] === 'clear') {
|
|
1791
|
+
if (!goalCondition) { err(dim(' no active goal\n')); return; }
|
|
1792
|
+
goalCondition = undefined; goalTurns = 0; goalTokens = 0; goalLastReason = undefined;
|
|
1793
|
+
persistGoal();
|
|
1794
|
+
err(green(' ✓ goal cleared\n'));
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
goalCondition = a.join(' ');
|
|
1798
|
+
goalTurns = 0; goalTokens = 0; goalLastReason = undefined;
|
|
1799
|
+
persistGoal();
|
|
1800
|
+
err(green(` ◎ goal set: ${goalCondition}\n`) + dim(' working… (Esc to pause)\n'));
|
|
1801
|
+
const tokensBefore = session.meta.tokens ?? 0;
|
|
1802
|
+
await turn(goalCondition);
|
|
1803
|
+
goalTokens += (session.meta.tokens ?? 0) - tokensBefore;
|
|
1804
|
+
goalTurns++;
|
|
1805
|
+
persistGoal();
|
|
1806
|
+
if (!exitRequested) await goalLoop();
|
|
1807
|
+
if (exitRequested) return true;
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1699
1810
|
exit: { desc: 'quit', run: () => true },
|
|
1700
1811
|
quit: { desc: 'quit', run: () => true },
|
|
1701
1812
|
};
|
|
@@ -1762,7 +1873,6 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1762
1873
|
|
|
1763
1874
|
let prefill: string | undefined; // set by double-Esc jump-back → pre-fills the next prompt
|
|
1764
1875
|
let tick = 0; // footer spinner frame counter (advances on each ticked re-render)
|
|
1765
|
-
|
|
1766
1876
|
/** One dispatch seam for a submitted line — typed (REPL loop) and spoken (voiceIO.onUtterance)
|
|
1767
1877
|
* share it, so voice gets !cmd/#note//commands/mentions/persistence identically. Returns 'quit'
|
|
1768
1878
|
* when a builtin asked to exit. */
|
|
@@ -1779,7 +1889,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1779
1889
|
// `#note` — jot a memory, like CC. Writes to the memory index (created if absent).
|
|
1780
1890
|
if (line.startsWith('#')) {
|
|
1781
1891
|
const note = line.slice(1).trim();
|
|
1782
|
-
if (note) { const where = await appendMemoryNote(agent.options.fs!, agent.options.memoryDir
|
|
1892
|
+
if (note) { const where = await appendMemoryNote(agent.options.fs!, primaryMemDir(agent.options.memoryDir, adot('memory')), note); err(green(` ✎ remembered → ${where}\n`)); }
|
|
1783
1893
|
return;
|
|
1784
1894
|
}
|
|
1785
1895
|
|
|
@@ -1805,6 +1915,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1805
1915
|
const task = pendingImages.length ? `${line} ${pendingImages.map((p) => '@' + p).join(' ')}` : line;
|
|
1806
1916
|
pendingImages.length = 0;
|
|
1807
1917
|
await turn(task);
|
|
1918
|
+
if (goalCondition && !aborting && !exitRequested) { goalTurns++; persistGoal(); await goalLoop(); }
|
|
1808
1919
|
if (exitRequested) return 'quit';
|
|
1809
1920
|
};
|
|
1810
1921
|
|
|
@@ -1893,6 +2004,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1893
2004
|
if (posture !== 'default') parts.push(postureLabel());
|
|
1894
2005
|
const r = work.reasoning; if (r && r !== 'off') parts.push(`reasoning:${r}`);
|
|
1895
2006
|
if (verboseOutput) parts.push('verbose');
|
|
2007
|
+
if (goalCondition) parts.push(`◎ goal (${goalTurns} turns)`);
|
|
1896
2008
|
if (inputStash.length) parts.push(`${inputStash.length} stashed (⌃S to pop)`);
|
|
1897
2009
|
// Running background tasks: one STACKED line each, pinned to the prompt block, with an animated
|
|
1898
2010
|
// spinner frame (statusTickMs re-renders this footer every second) — a slow worker is visibly
|
|
@@ -1905,8 +2017,16 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1905
2017
|
}
|
|
1906
2018
|
return [...taskLines, parts.join(' · ')].filter(Boolean).join('\n');
|
|
1907
2019
|
};
|
|
2020
|
+
// LIVE prompt: while a worker question is pending, the prompt itself becomes the question
|
|
2021
|
+
// (CC-parity for permission asks) — duplexAsk's redrawNow() flips it the moment an ask parks.
|
|
2022
|
+
const livePrompt = () => {
|
|
2023
|
+
const ask = dx?.pendingAsks.size ? dx.pendingAsks.values().next().value : undefined;
|
|
2024
|
+
if (!ask) return promptStr;
|
|
2025
|
+
const q = ask.question.replace(/\s+/g, ' ').slice(0, 64);
|
|
2026
|
+
return bold(yellow(`? ${q}${ask.question.length > 64 ? '…' : ''} ‹yes/no› `));
|
|
2027
|
+
};
|
|
1908
2028
|
const result = await readMultiline((cont) => editor.readLine({
|
|
1909
|
-
prompt: cont ? contPrompt :
|
|
2029
|
+
prompt: cont ? contPrompt : livePrompt, suggest, history, classifyPaste, onEmptyPaste: grabClipboardAttachment,
|
|
1910
2030
|
initial: cont ? undefined : initial, status: computeFooter, vimMode: cfg.editorMode === 'vim',
|
|
1911
2031
|
statusTickMs: dx ? 1000 : undefined, // duplex: animate the running-task footer while idle at the prompt
|
|
1912
2032
|
onCyclePosture: cyclePosture,
|
|
@@ -1920,6 +2040,14 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1920
2040
|
if (result === REWIND) { prefill = await rewindToMessage(); continue; } // double-Esc → jump-back picker
|
|
1921
2041
|
const line = result.trim();
|
|
1922
2042
|
if (!line) continue;
|
|
2043
|
+
// Typed answer to a pending worker question: resolve it DIRECTLY (deterministic, no LLM hop) —
|
|
2044
|
+
// same UX as approving in a normal session. Anything else falls through to the conversation.
|
|
2045
|
+
if (dx?.pendingAsks.size && /^(y(es|ep|eah)?|n(o|ope)?|sure|ok(ay)?|allow|deny|go( ahead)?)[.!]?$/i.test(line)) {
|
|
2046
|
+
const [id, ask] = dx.pendingAsks.entries().next().value!;
|
|
2047
|
+
ask.resolve(line);
|
|
2048
|
+
err(dim(` ↳ answered ${id}: ${line}\n`));
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
1923
2051
|
let quit = await dispatchLine(line) === 'quit';
|
|
1924
2052
|
// Drain stashed input (typed while the turn was running)
|
|
1925
2053
|
while (!quit && inputStash.length) {
|
|
@@ -2017,7 +2145,8 @@ async function main() {
|
|
|
2017
2145
|
const { ok, res } = await runTurn(agent, store, session, args.task, undefined, cwd);
|
|
2018
2146
|
// opt-in: on a bad outcome, reflect once and persist a novel lesson for next session
|
|
2019
2147
|
if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
|
|
2020
|
-
const
|
|
2148
|
+
const _fsBase = agent.options.fs!.getCwd() === '/' ? '' : agent.options.fs!.getCwd();
|
|
2149
|
+
const slug = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs!, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
|
|
2021
2150
|
if (slug) err(dim(` ✎ learned a lesson → ${slug}\n`));
|
|
2022
2151
|
}
|
|
2023
2152
|
await closeMcp(mounted); // kill MCP child processes after the run (and any reflection)
|
|
@@ -229,8 +229,11 @@ declare class AgentOptions {
|
|
|
229
229
|
skillsDir?: string | string[];
|
|
230
230
|
/** VFS dir(s) of slash-command templates (`<dir>/<name>.md`). If set: inject a catalog + add the `SlashCommand` tool. Multiple dirs are merged (first wins). */
|
|
231
231
|
commandsDir?: string | string[];
|
|
232
|
-
/** VFS dir of memory (`<dir>/MEMORY.md`). If set: inject the index at run start (persistence = backend).
|
|
233
|
-
|
|
232
|
+
/** VFS dir(s) of memory (`<dir>/MEMORY.md`). If set: inject the index at run start (persistence = backend).
|
|
233
|
+
* Multiple dirs are merged (reads search all; writes go to first). */
|
|
234
|
+
memoryDir?: string | string[];
|
|
235
|
+
/** User-scope memory dir for global facts (type=user/feedback). Remember routes by type when set. */
|
|
236
|
+
memoryUserDir?: string;
|
|
234
237
|
/** Filenames to discover as project instructions (e.g. `AGENT.md`, `AGENTS.md`, `CLAUDE.md`).
|
|
235
238
|
* Walks the VFS tree and merges all found files (general → specific, like Claude Code).
|
|
236
239
|
* `true` (default) = auto-discover standard names. `string[]` = custom names. `false` = skip. */
|
|
@@ -272,6 +275,8 @@ declare class AgentOptions {
|
|
|
272
275
|
};
|
|
273
276
|
/** Provider-specific options forwarded to ai.chat() (e.g. cursor mcpServers, cwd). */
|
|
274
277
|
providerOptions?: Record<string, unknown>;
|
|
278
|
+
/** Tool selection mode: 'auto' = model decides (needed for Groq); undefined = provider default. */
|
|
279
|
+
toolChoice?: 'auto' | 'required' | 'none';
|
|
275
280
|
/** Extended-thinking / reasoning effort, normalized across providers (anthropic, openai).
|
|
276
281
|
* `'off'`/undefined = none; `'low'|'medium'|'high'` or a raw token budget. Mapped to the
|
|
277
282
|
* provider-specific request shape via {@link reasoningToChatFragment}; explicit `providerOptions` wins. */
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { h as RunResult, R as ReasoningEffort } from './Agent-
|
|
2
|
+
import { h as RunResult, R as ReasoningEffort } from './Agent-B_xvSHlG.js';
|
|
3
3
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
4
4
|
import { M as Message, c as ContentPart } from './tools-GPWp7oXq.js';
|
|
5
5
|
|