agent-sh 0.13.3 → 0.13.5
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 +2 -0
- package/dist/agent/agent-loop.js +115 -87
- package/dist/agent/index.js +5 -12
- package/dist/agent/providers/openai.d.ts +2 -0
- package/dist/agent/providers/openai.js +9 -2
- package/examples/extensions/ash-acp-bridge/src/index.ts +5 -2
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/frontend.ts +95 -22
- package/examples/extensions/ashi/src/multi-session-store.ts +8 -0
- package/examples/extensions/ollama.ts +19 -17
- package/examples/extensions/zai-coding-plan.ts +2 -1
- package/package.json +1 -1
- package/examples/extensions/ollama-cloud.ts +0 -78
|
@@ -42,6 +42,7 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
42
42
|
private ctorListeners;
|
|
43
43
|
private ctorPipeListeners;
|
|
44
44
|
private lastProjectSkillNames;
|
|
45
|
+
private lastAgentInfo;
|
|
45
46
|
private sessionStartTime;
|
|
46
47
|
private toolCallCounts;
|
|
47
48
|
private totalToolCalls;
|
|
@@ -99,6 +100,7 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
99
100
|
private cancel;
|
|
100
101
|
private reasoningParams;
|
|
101
102
|
private get currentMode();
|
|
103
|
+
private emitAgentInfoIfChanged;
|
|
102
104
|
private get currentModel();
|
|
103
105
|
/**
|
|
104
106
|
* Run compaction via the `conversation:compact` handler. After any
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -60,6 +60,7 @@ export class AgentLoop {
|
|
|
60
60
|
ctorListeners = [];
|
|
61
61
|
ctorPipeListeners = [];
|
|
62
62
|
lastProjectSkillNames = new Set();
|
|
63
|
+
lastAgentInfo = null;
|
|
63
64
|
// ── Session telemetry — behavioral self-awareness ──────────────
|
|
64
65
|
// Every ash deserves to know what it's been doing. This tracks the
|
|
65
66
|
// agent's own behavioral patterns across the session: which tools
|
|
@@ -87,7 +88,7 @@ export class AgentLoop {
|
|
|
87
88
|
// doing X." Addresses Q3 in QUESTIONS.md.
|
|
88
89
|
lastErrorByTool = new Map(); // tool → error summary
|
|
89
90
|
lastErrorByFile = new Map(); // file path → error summary
|
|
90
|
-
static THINKING_LEVELS = ["off", "low", "medium", "high"];
|
|
91
|
+
static THINKING_LEVELS = ["off", "low", "medium", "high", "xhigh"];
|
|
91
92
|
bus;
|
|
92
93
|
llmClient;
|
|
93
94
|
handlers;
|
|
@@ -168,24 +169,20 @@ export class AgentLoop {
|
|
|
168
169
|
];
|
|
169
170
|
if (prev) {
|
|
170
171
|
const newIdx = this.modes.findIndex((m) => m.model === prev.model && m.provider === prev.provider);
|
|
171
|
-
if (newIdx !== -1)
|
|
172
|
+
if (newIdx !== -1) {
|
|
172
173
|
this.currentModeIndex = newIdx;
|
|
174
|
+
const next = this.modes[newIdx];
|
|
175
|
+
if (next.providerConfig && next.providerConfig !== prev.providerConfig) {
|
|
176
|
+
this.llmClient.reconfigure({ ...next.providerConfig, model: next.model });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
173
179
|
}
|
|
174
180
|
if (activePreserved && prev) {
|
|
175
181
|
this.bus.emit("ui:info", {
|
|
176
182
|
message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
|
|
177
183
|
});
|
|
178
184
|
}
|
|
179
|
-
|
|
180
|
-
if (active && active.contextWindow !== prev?.contextWindow) {
|
|
181
|
-
this.bus.emit("agent:info", {
|
|
182
|
-
name: "ash",
|
|
183
|
-
version: PACKAGE_VERSION,
|
|
184
|
-
model: active.model,
|
|
185
|
-
provider: active.provider,
|
|
186
|
-
contextWindow: active.contextWindow,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
185
|
+
this.emitAgentInfoIfChanged();
|
|
189
186
|
this.bus.emit("config:changed", {});
|
|
190
187
|
});
|
|
191
188
|
// Fires before wire() too — agent-backend emits this from
|
|
@@ -203,6 +200,7 @@ export class AgentLoop {
|
|
|
203
200
|
else {
|
|
204
201
|
this.llmClient.model = m.model;
|
|
205
202
|
}
|
|
203
|
+
this.emitAgentInfoIfChanged();
|
|
206
204
|
this.bus.emit("config:changed", {});
|
|
207
205
|
});
|
|
208
206
|
const getToolsPipe = () => ({ tools: this.getTools() });
|
|
@@ -251,7 +249,7 @@ export class AgentLoop {
|
|
|
251
249
|
this.llmClient.model = m.model;
|
|
252
250
|
}
|
|
253
251
|
const label = m.provider ? `${m.provider}: ${m.model}` : m.model;
|
|
254
|
-
this.
|
|
252
|
+
this.emitAgentInfoIfChanged();
|
|
255
253
|
// Persist as the new default — selection survives restart.
|
|
256
254
|
// Safe even for dynamic providers: agent-backend defers mode
|
|
257
255
|
// resolution to `core:extensions-loaded`, so the extension gets
|
|
@@ -370,6 +368,8 @@ export class AgentLoop {
|
|
|
370
368
|
this.bus.emit("conversation:message-appended", { role: "system", content: note });
|
|
371
369
|
}
|
|
372
370
|
});
|
|
371
|
+
this.lastAgentInfo = null;
|
|
372
|
+
this.emitAgentInfoIfChanged();
|
|
373
373
|
}
|
|
374
374
|
/** Unsubscribe from bus events — deactivates this backend. */
|
|
375
375
|
unwire() {
|
|
@@ -507,11 +507,29 @@ export class AgentLoop {
|
|
|
507
507
|
return mode.buildReasoningParams(this.thinkingLevel);
|
|
508
508
|
if (this.thinkingLevel === "off")
|
|
509
509
|
return {};
|
|
510
|
-
|
|
510
|
+
const effort = this.thinkingLevel === "xhigh" ? "high" : this.thinkingLevel;
|
|
511
|
+
return { reasoning_effort: effort };
|
|
511
512
|
}
|
|
512
513
|
get currentMode() {
|
|
513
514
|
return this.modes[this.currentModeIndex];
|
|
514
515
|
}
|
|
516
|
+
emitAgentInfoIfChanged() {
|
|
517
|
+
const m = this.modes[this.currentModeIndex];
|
|
518
|
+
if (!m)
|
|
519
|
+
return;
|
|
520
|
+
const prev = this.lastAgentInfo;
|
|
521
|
+
if (prev && prev.model === m.model && prev.provider === m.provider && prev.contextWindow === m.contextWindow) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
this.lastAgentInfo = { model: m.model, provider: m.provider, contextWindow: m.contextWindow };
|
|
525
|
+
this.bus.emit("agent:info", {
|
|
526
|
+
name: "ash",
|
|
527
|
+
version: PACKAGE_VERSION,
|
|
528
|
+
model: m.model,
|
|
529
|
+
provider: m.provider,
|
|
530
|
+
contextWindow: m.contextWindow,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
515
533
|
get currentModel() {
|
|
516
534
|
return this.modes[this.currentModeIndex].model;
|
|
517
535
|
}
|
|
@@ -1076,12 +1094,15 @@ export class AgentLoop {
|
|
|
1076
1094
|
streamedCalls: streamedToolCalls,
|
|
1077
1095
|
});
|
|
1078
1096
|
fullResponseText += text;
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1097
|
+
if (text || toolCalls.length > 0) {
|
|
1098
|
+
this.toolProtocol.recordAssistant(this.conversation, text, toolCalls, extras);
|
|
1099
|
+
this.bus.emit("conversation:message-appended", {
|
|
1100
|
+
role: "assistant",
|
|
1101
|
+
content: text,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
if (signal.aborted)
|
|
1105
|
+
break;
|
|
1085
1106
|
// No tool calls → agent is done
|
|
1086
1107
|
if (toolCalls.length === 0) {
|
|
1087
1108
|
this.conversation.eagerNucleateAgent(fullResponseText);
|
|
@@ -1502,83 +1523,90 @@ export class AgentLoop {
|
|
|
1502
1523
|
};
|
|
1503
1524
|
this.bus.emit("llm:request", requestParams);
|
|
1504
1525
|
const stream = await this.llmClient.stream({ ...requestParams, signal });
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
// Feed accurate token count back to conversation state
|
|
1519
|
-
if (promptTokens > 0) {
|
|
1520
|
-
this.conversation.updateApiTokenCount(promptTokens);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
const choice = chunk.choices[0];
|
|
1524
|
-
if (!choice)
|
|
1525
|
-
continue;
|
|
1526
|
-
const delta = choice.delta;
|
|
1527
|
-
// Text content
|
|
1528
|
-
if (delta?.content) {
|
|
1529
|
-
text += delta.content;
|
|
1530
|
-
// Filter tool tags from display output (inline mode)
|
|
1531
|
-
const displayText = streamFilter
|
|
1532
|
-
? streamFilter.feed(delta.content)
|
|
1533
|
-
: delta.content;
|
|
1534
|
-
if (displayText) {
|
|
1535
|
-
this.bus.emitTransform("agent:response-chunk", {
|
|
1536
|
-
blocks: [{ type: "text", text: displayText }],
|
|
1526
|
+
try {
|
|
1527
|
+
for await (const chunk of stream) {
|
|
1528
|
+
if (signal.aborted)
|
|
1529
|
+
break;
|
|
1530
|
+
this.bus.emit("llm:chunk", { chunk });
|
|
1531
|
+
// Token usage (may arrive in a chunk with empty choices)
|
|
1532
|
+
if (chunk.usage) {
|
|
1533
|
+
const u = chunk.usage;
|
|
1534
|
+
const promptTokens = u.prompt_tokens ?? 0;
|
|
1535
|
+
this.bus.emit("agent:usage", {
|
|
1536
|
+
prompt_tokens: promptTokens,
|
|
1537
|
+
completion_tokens: u.completion_tokens ?? 0,
|
|
1538
|
+
total_tokens: u.total_tokens ?? 0,
|
|
1537
1539
|
});
|
|
1540
|
+
// Feed accurate token count back to conversation state
|
|
1541
|
+
if (promptTokens > 0) {
|
|
1542
|
+
this.conversation.updateApiTokenCount(promptTokens);
|
|
1543
|
+
}
|
|
1538
1544
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1545
|
+
const choice = chunk.choices[0];
|
|
1546
|
+
if (!choice)
|
|
1547
|
+
continue;
|
|
1548
|
+
const delta = choice.delta;
|
|
1549
|
+
// Text content
|
|
1550
|
+
if (delta?.content) {
|
|
1551
|
+
text += delta.content;
|
|
1552
|
+
// Filter tool tags from display output (inline mode)
|
|
1553
|
+
const displayText = streamFilter
|
|
1554
|
+
? streamFilter.feed(delta.content)
|
|
1555
|
+
: delta.content;
|
|
1556
|
+
if (displayText) {
|
|
1557
|
+
this.bus.emitTransform("agent:response-chunk", {
|
|
1558
|
+
blocks: [{ type: "text", text: displayText }],
|
|
1559
|
+
});
|
|
1554
1560
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
+
}
|
|
1562
|
+
const d = delta;
|
|
1563
|
+
for (const name of ["reasoning", "reasoning_content"]) {
|
|
1564
|
+
if (typeof d?.[name] === "string" && d[name].length > 0) {
|
|
1565
|
+
reasoning += d[name];
|
|
1566
|
+
reasoningField ??= name;
|
|
1567
|
+
this.bus.emit("agent:thinking-chunk", { text: d[name] });
|
|
1561
1568
|
}
|
|
1562
1569
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1570
|
+
if (Array.isArray(d?.reasoning_details)) {
|
|
1571
|
+
for (const x of d.reasoning_details) {
|
|
1572
|
+
const idx = typeof x?.index === "number" ? x.index : reasoningDetailsByIndex.size;
|
|
1573
|
+
const prev = reasoningDetailsByIndex.get(idx);
|
|
1574
|
+
if (!prev) {
|
|
1575
|
+
reasoningDetailsByIndex.set(idx, { ...x });
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
if (typeof x.text === "string")
|
|
1579
|
+
prev.text = (prev.text ?? "") + x.text;
|
|
1580
|
+
for (const [k, v] of Object.entries(x))
|
|
1581
|
+
if (k !== "text" && prev[k] === undefined)
|
|
1582
|
+
prev[k] = v;
|
|
1583
|
+
}
|
|
1574
1584
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1585
|
+
}
|
|
1586
|
+
// Tool calls (streamed incrementally)
|
|
1587
|
+
if (delta?.tool_calls) {
|
|
1588
|
+
for (const tc of delta.tool_calls) {
|
|
1589
|
+
const idx = tc.index;
|
|
1590
|
+
if (!pendingToolCalls[idx]) {
|
|
1591
|
+
pendingToolCalls[idx] = {
|
|
1592
|
+
id: tc.id,
|
|
1593
|
+
name: tc.function.name,
|
|
1594
|
+
argumentsJson: "",
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
if (tc.function?.arguments) {
|
|
1598
|
+
pendingToolCalls[idx].argumentsJson +=
|
|
1599
|
+
tc.function.arguments;
|
|
1600
|
+
}
|
|
1578
1601
|
}
|
|
1579
1602
|
}
|
|
1580
1603
|
}
|
|
1581
1604
|
}
|
|
1605
|
+
catch (e) {
|
|
1606
|
+
// On abort, fall through with whatever was accumulated so far.
|
|
1607
|
+
if (!signal.aborted)
|
|
1608
|
+
throw e;
|
|
1609
|
+
}
|
|
1582
1610
|
// Flush any buffered content from the stream filter
|
|
1583
1611
|
if (streamFilter) {
|
|
1584
1612
|
const remaining = streamFilter.flush();
|
package/dist/agent/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import { AgentLoop } from "./agent-loop.js";
|
|
|
2
2
|
import { LlmClient } from "../utils/llm-client.js";
|
|
3
3
|
import { createLlmFacade } from "../utils/llm-facade.js";
|
|
4
4
|
import { resolveProvider, getProviderNames, getSettings } from "../core/settings.js";
|
|
5
|
-
import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
6
5
|
import { discoverSkills } from "./skills.js";
|
|
7
6
|
import { resolveApiKey } from "../cli/auth/keys.js";
|
|
8
7
|
import activateOpenrouter from "./providers/openrouter.js";
|
|
@@ -15,7 +14,9 @@ function persistedModelFor(providerName) {
|
|
|
15
14
|
return getSettings().providers?.[providerName]?.defaultModel;
|
|
16
15
|
}
|
|
17
16
|
function defaultReasoningBuilder(level) {
|
|
18
|
-
|
|
17
|
+
if (level === "off")
|
|
18
|
+
return {};
|
|
19
|
+
return { reasoning_effort: level === "xhigh" ? "high" : level };
|
|
19
20
|
}
|
|
20
21
|
function mergeCaps(settingsCaps, payloadCaps, modelIds) {
|
|
21
22
|
if (!settingsCaps)
|
|
@@ -118,11 +119,12 @@ export default function agentBackend(ctx) {
|
|
|
118
119
|
ctx.define("llm:get-client", () => llmClient);
|
|
119
120
|
ctx.define("llm:invoke", (messages, opts) => {
|
|
120
121
|
const effort = opts?.reasoningEffort;
|
|
122
|
+
const clampedEffort = effort === "xhigh" ? "high" : effort;
|
|
121
123
|
return llmClient.complete({
|
|
122
124
|
messages: messages,
|
|
123
125
|
max_tokens: opts?.maxTokens,
|
|
124
126
|
model: opts?.model,
|
|
125
|
-
...(
|
|
127
|
+
...(clampedEffort && clampedEffort !== "off" ? { reasoning_effort: clampedEffort } : {}),
|
|
126
128
|
});
|
|
127
129
|
});
|
|
128
130
|
let modes = [];
|
|
@@ -224,13 +226,6 @@ export default function agentBackend(ctx) {
|
|
|
224
226
|
});
|
|
225
227
|
},
|
|
226
228
|
});
|
|
227
|
-
bus.emit("agent:info", {
|
|
228
|
-
name: "ash",
|
|
229
|
-
version: PACKAGE_VERSION,
|
|
230
|
-
model: llmClient.model,
|
|
231
|
-
provider: modes[initialModeIndex]?.provider,
|
|
232
|
-
contextWindow: modes[initialModeIndex]?.contextWindow,
|
|
233
|
-
});
|
|
234
229
|
},
|
|
235
230
|
});
|
|
236
231
|
});
|
|
@@ -327,9 +322,7 @@ export default function agentBackend(ctx) {
|
|
|
327
322
|
};
|
|
328
323
|
});
|
|
329
324
|
bus.emit("config:set-modes", { modes: newModes });
|
|
330
|
-
bus.emit("agent:info", { name: "ash", version: PACKAGE_VERSION, model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
331
325
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
332
|
-
bus.emit("config:changed", {});
|
|
333
326
|
});
|
|
334
327
|
bus.onPipe("banner:collect", (e) => {
|
|
335
328
|
if (e.activeBackend && e.activeBackend !== "ash")
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Cloud OpenAI (api.openai.com). reasoning_effort vocabulary diverges per
|
|
3
3
|
* family: o-series has no off; gpt-5-codex floors at "low"; plain gpt-5
|
|
4
4
|
* floors at "minimal"; gpt-5.1+ accepts "none" as documented full off.
|
|
5
|
+
* Top tier: only gpt-5.1-codex-max and gpt-5.[4-9]+ accept "xhigh"; others
|
|
6
|
+
* clamp to "high".
|
|
5
7
|
*/
|
|
6
8
|
import type { AgentContext } from "../host-types.js";
|
|
7
9
|
export default function activate(ctx: AgentContext): void;
|
|
@@ -18,9 +18,16 @@ function offEffortFor(model) {
|
|
|
18
18
|
return "minimal";
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
+
function supportsXhigh(model) {
|
|
22
|
+
if (model.startsWith("gpt-5.1-codex-max"))
|
|
23
|
+
return true;
|
|
24
|
+
return /^gpt-5\.[4-9]/.test(model);
|
|
25
|
+
}
|
|
21
26
|
function buildReasoningParams(level, model) {
|
|
22
|
-
if (level !== "off")
|
|
23
|
-
|
|
27
|
+
if (level !== "off") {
|
|
28
|
+
const effort = level === "xhigh" && !(model && supportsXhigh(model)) ? "high" : level;
|
|
29
|
+
return { reasoning_effort: effort };
|
|
30
|
+
}
|
|
24
31
|
const off = model ? offEffortFor(model) : null;
|
|
25
32
|
return off ? { reasoning_effort: off } : {};
|
|
26
33
|
}
|
|
@@ -446,10 +446,13 @@ function getModelsPayload(): Record<string, unknown> | undefined {
|
|
|
446
446
|
if (!core) return undefined;
|
|
447
447
|
const info = core.bus.emitPipe("config:get-models", { models: [], active: null });
|
|
448
448
|
if (!info.models.length) return undefined;
|
|
449
|
+
const idFor = (m: { model: string; provider: string }) =>
|
|
450
|
+
m.provider ? `${m.model}@${m.provider}` : m.model;
|
|
451
|
+
const current = info.active ?? info.models[0]!;
|
|
449
452
|
return {
|
|
450
|
-
currentModelId:
|
|
453
|
+
currentModelId: idFor(current),
|
|
451
454
|
availableModels: info.models.map((m) => ({
|
|
452
|
-
modelId: m
|
|
455
|
+
modelId: idFor(m),
|
|
453
456
|
name: m.provider ? `${m.provider}/${m.model}` : m.model,
|
|
454
457
|
description: m.provider ? `Provider: ${m.provider}` : "",
|
|
455
458
|
})),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guanyilun/ashi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@earendil-works/pi-tui": "^0.74.0",
|
|
51
|
-
"agent-sh": "^0.13.
|
|
51
|
+
"agent-sh": "^0.13.3",
|
|
52
52
|
"chalk": "^5.5.0",
|
|
53
53
|
"cli-highlight": "^2.1.11"
|
|
54
54
|
},
|
|
@@ -175,6 +175,7 @@ export function mountAshi(
|
|
|
175
175
|
|
|
176
176
|
const chat = new Container();
|
|
177
177
|
const footerSlot = new Container();
|
|
178
|
+
const queueSlot = new Container();
|
|
178
179
|
const editor = new Editor(tui, editorTheme(), { paddingX: 1 });
|
|
179
180
|
editor.setAutocompleteProvider(new BusAutocompleteProvider(bus));
|
|
180
181
|
editor.onSubmit = (text) => {
|
|
@@ -188,6 +189,12 @@ export function mountAshi(
|
|
|
188
189
|
bus.emit("command:execute", { name, args });
|
|
189
190
|
return;
|
|
190
191
|
}
|
|
192
|
+
if (processing) {
|
|
193
|
+
queuedQueries.push(query);
|
|
194
|
+
renderQueueSlot();
|
|
195
|
+
tui.requestRender();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
191
198
|
bus.emit("agent:submit", { query });
|
|
192
199
|
};
|
|
193
200
|
|
|
@@ -211,6 +218,7 @@ export function mountAshi(
|
|
|
211
218
|
|
|
212
219
|
tui.addChild(chat);
|
|
213
220
|
tui.addChild(footerSlot);
|
|
221
|
+
tui.addChild(queueSlot);
|
|
214
222
|
tui.addChild(editor);
|
|
215
223
|
tui.addChild(statusFooter);
|
|
216
224
|
tui.setFocus(editor);
|
|
@@ -227,6 +235,16 @@ export function mountAshi(
|
|
|
227
235
|
let lastToolResult: ToolResultView | null = null;
|
|
228
236
|
let loader: Loader | null = null;
|
|
229
237
|
let processing = false;
|
|
238
|
+
const queuedQueries: string[] = [];
|
|
239
|
+
|
|
240
|
+
const renderQueueSlot = (): void => {
|
|
241
|
+
queueSlot.clear();
|
|
242
|
+
for (const q of queuedQueries) {
|
|
243
|
+
const oneLine = q.replace(/\s+/g, " ");
|
|
244
|
+
const preview = oneLine.length > 80 ? oneLine.slice(0, 77) + "…" : oneLine;
|
|
245
|
+
queueSlot.addChild(new InfoLine(`↳ queued: ${preview}`));
|
|
246
|
+
}
|
|
247
|
+
};
|
|
230
248
|
let hideThinking = true;
|
|
231
249
|
|
|
232
250
|
const renderState = (): { state: Record<string, unknown>; invalidate: () => void } => ({
|
|
@@ -552,6 +570,11 @@ export function mountAshi(
|
|
|
552
570
|
chat.addChild(new Spacer(1));
|
|
553
571
|
refreshFooterStats();
|
|
554
572
|
refreshBranch();
|
|
573
|
+
const next = queuedQueries.shift();
|
|
574
|
+
if (next !== undefined) {
|
|
575
|
+
renderQueueSlot();
|
|
576
|
+
bus.emit("agent:submit", { query: next });
|
|
577
|
+
}
|
|
555
578
|
tui.requestRender();
|
|
556
579
|
});
|
|
557
580
|
|
|
@@ -612,6 +635,9 @@ export function mountAshi(
|
|
|
612
635
|
|
|
613
636
|
// ── Pickers ────────────────────────────────────────────────────
|
|
614
637
|
let pickerOpen = false;
|
|
638
|
+
let activeSessionPicker: SelectList | null = null;
|
|
639
|
+
let activeSessionRepopulate: ((keepIndex?: number) => boolean) | null = null;
|
|
640
|
+
let activeSessionClose: (() => void) | null = null;
|
|
615
641
|
|
|
616
642
|
const openTreePicker = async (): Promise<void> => {
|
|
617
643
|
if (pickerOpen) return;
|
|
@@ -657,38 +683,60 @@ export function mountAshi(
|
|
|
657
683
|
|
|
658
684
|
const openSessionPicker = async (): Promise<void> => {
|
|
659
685
|
if (pickerOpen) return;
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
if (list.length === 0) {
|
|
663
|
-
bus.emit("ui:info", { message: "no past sessions in this cwd" });
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const items: SelectItem[] = list.map((s) => ({
|
|
667
|
-
value: s.id,
|
|
668
|
-
label: formatSessionRow(s, false),
|
|
669
|
-
}));
|
|
670
|
-
const picker = new SelectList(items, 15, selectListTheme());
|
|
686
|
+
|
|
687
|
+
const hint = new InfoLine("↑↓ move · enter: resume · d: delete · esc: cancel");
|
|
671
688
|
|
|
672
689
|
const close = (): void => {
|
|
690
|
+
if (activeSessionPicker) footerSlot.removeChild(activeSessionPicker);
|
|
691
|
+
footerSlot.removeChild(hint);
|
|
692
|
+
activeSessionPicker = null;
|
|
693
|
+
activeSessionRepopulate = null;
|
|
694
|
+
activeSessionClose = null;
|
|
673
695
|
pickerOpen = false;
|
|
674
|
-
footerSlot.removeChild(picker);
|
|
675
696
|
tui.setFocus(editor);
|
|
676
697
|
tui.requestRender();
|
|
677
698
|
};
|
|
678
699
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
700
|
+
const populate = (keepIndex?: number): boolean => {
|
|
701
|
+
if (activeSessionPicker) footerSlot.removeChild(activeSessionPicker);
|
|
702
|
+
const currentId = getStore().current().id;
|
|
703
|
+
const list = getStore().listSessions().filter((s) => s.id !== currentId);
|
|
704
|
+
if (list.length === 0) {
|
|
705
|
+
activeSessionPicker = null;
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const items: SelectItem[] = list.map((s) => ({
|
|
709
|
+
value: s.id,
|
|
710
|
+
label: formatSessionRow(s, false),
|
|
711
|
+
}));
|
|
712
|
+
const picker = new SelectList(items, 15, selectListTheme());
|
|
713
|
+
if (keepIndex !== undefined) {
|
|
714
|
+
picker.setSelectedIndex(Math.min(keepIndex, items.length - 1));
|
|
715
|
+
}
|
|
716
|
+
picker.onSelect = async (item) => {
|
|
717
|
+
const id = item.value;
|
|
718
|
+
close();
|
|
719
|
+
resumeSession(ctx, getStore, capture, id);
|
|
720
|
+
bus.emit("ui:info", { message: `resumed session ${id}` });
|
|
721
|
+
await rebuildChat();
|
|
722
|
+
refreshFooterStats();
|
|
723
|
+
};
|
|
724
|
+
picker.onCancel = close;
|
|
725
|
+
activeSessionPicker = picker;
|
|
726
|
+
footerSlot.addChild(picker);
|
|
727
|
+
tui.setFocus(picker);
|
|
728
|
+
return true;
|
|
686
729
|
};
|
|
687
|
-
picker.onCancel = close;
|
|
688
730
|
|
|
731
|
+
footerSlot.addChild(hint);
|
|
732
|
+
if (!populate()) {
|
|
733
|
+
footerSlot.removeChild(hint);
|
|
734
|
+
bus.emit("ui:info", { message: "no past sessions in this cwd" });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
689
737
|
pickerOpen = true;
|
|
690
|
-
|
|
691
|
-
|
|
738
|
+
activeSessionRepopulate = populate;
|
|
739
|
+
activeSessionClose = close;
|
|
692
740
|
tui.requestRender();
|
|
693
741
|
};
|
|
694
742
|
|
|
@@ -711,6 +759,31 @@ export function mountAshi(
|
|
|
711
759
|
bus.emit("agent:cancel-request", {});
|
|
712
760
|
return { consume: true };
|
|
713
761
|
}
|
|
762
|
+
if (activeSessionPicker && matchesKey(data, "d")) {
|
|
763
|
+
const selected = activeSessionPicker.getSelectedItem();
|
|
764
|
+
if (selected) {
|
|
765
|
+
const currentId = getStore().current().id;
|
|
766
|
+
const idx = getStore().listSessions()
|
|
767
|
+
.filter((s) => s.id !== currentId)
|
|
768
|
+
.findIndex((s) => s.id === selected.value);
|
|
769
|
+
try {
|
|
770
|
+
getStore().deleteSession(selected.value);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
bus.emit("ui:error", { message: `delete failed: ${(e as Error).message}` });
|
|
773
|
+
return { consume: true };
|
|
774
|
+
}
|
|
775
|
+
if (!activeSessionRepopulate?.(idx)) activeSessionClose?.();
|
|
776
|
+
tui.requestRender();
|
|
777
|
+
}
|
|
778
|
+
return { consume: true };
|
|
779
|
+
}
|
|
780
|
+
if (matchesKey(data, "up") && queuedQueries.length > 0 && editor.getText().length === 0) {
|
|
781
|
+
const last = queuedQueries.pop()!;
|
|
782
|
+
renderQueueSlot();
|
|
783
|
+
editor.setText(last);
|
|
784
|
+
tui.requestRender();
|
|
785
|
+
return { consume: true };
|
|
786
|
+
}
|
|
714
787
|
if (matchesKey(data, "ctrl+c")) {
|
|
715
788
|
editor.setText("");
|
|
716
789
|
return { consume: true };
|
|
@@ -81,6 +81,14 @@ export class MultiSessionStore {
|
|
|
81
81
|
return this.currentStore;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
deleteSession(id: string): void {
|
|
85
|
+
if (id === this.currentStore.id) throw new Error("cannot delete the active session");
|
|
86
|
+
const filePath = this.sessionFile(id);
|
|
87
|
+
for (const p of [filePath, filePath + ".leaf", filePath + ".meta"]) {
|
|
88
|
+
try { fs.unlinkSync(p); } catch { /* missing siblings are fine */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
listSessions(): SessionInfo[] {
|
|
85
93
|
let names: string[];
|
|
86
94
|
try { names = fs.readdirSync(this.dir); } catch { return []; }
|
|
@@ -1,44 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ollama provider extension — local daemon
|
|
2
|
+
* Ollama provider extension — local daemon or Ollama Cloud.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Cloud auth (any of):
|
|
5
|
+
* agent-sh auth login ollama-cloud # preferred
|
|
6
|
+
* OLLAMA_API_KEY=... # env fallback
|
|
7
|
+
*
|
|
8
|
+
* Local host:
|
|
9
|
+
* OLLAMA_HOST (default http://localhost:11434)
|
|
6
10
|
*
|
|
7
11
|
* Catalog comes from /api/tags; per-model context length is fetched
|
|
8
12
|
* from /api/show (model_info["${arch}.context_length"]). Chat goes
|
|
9
13
|
* through the OpenAI-compatible /v1/chat/completions shim.
|
|
10
14
|
*
|
|
11
|
-
* Setup (cloud):
|
|
12
|
-
* export OLLAMA_API_KEY="your-key"
|
|
13
|
-
*
|
|
14
|
-
* Setup (local):
|
|
15
|
-
* ollama serve # default http://localhost:11434
|
|
16
|
-
*
|
|
17
15
|
* Usage:
|
|
18
16
|
* agent-sh -e ./examples/extensions/ollama.ts
|
|
19
17
|
*
|
|
20
18
|
* # Or add to settings.json:
|
|
21
19
|
* { "extensions": ["./examples/extensions/ollama.ts"] }
|
|
22
20
|
*/
|
|
23
|
-
import
|
|
21
|
+
import { resolveApiKey } from "agent-sh/auth";
|
|
22
|
+
import type { AgentContext } from "agent-sh/types";
|
|
24
23
|
|
|
25
24
|
const ECHO_REASONING_PATTERNS: RegExp[] = [/deepseek/i];
|
|
26
25
|
|
|
27
|
-
export default function activate(ctx:
|
|
28
|
-
const
|
|
29
|
-
const host =
|
|
26
|
+
export default function activate(ctx: AgentContext): void {
|
|
27
|
+
const cloudKey = resolveApiKey("ollama-cloud").key ?? process.env.OLLAMA_API_KEY;
|
|
28
|
+
const host = cloudKey
|
|
30
29
|
? "https://ollama.com"
|
|
31
30
|
: (process.env.OLLAMA_HOST ?? "http://localhost:11434").replace(/\/$/, "");
|
|
32
|
-
const id =
|
|
31
|
+
const id = cloudKey ? "ollama-cloud" : "ollama";
|
|
33
32
|
|
|
34
33
|
// OpenAI SDK rejects an empty apiKey; the local daemon ignores the value.
|
|
35
|
-
const sdkKey =
|
|
34
|
+
const sdkKey = cloudKey || "no-key";
|
|
36
35
|
const baseURL = `${host}/v1`;
|
|
37
36
|
const headers: Record<string, string> = {};
|
|
38
|
-
if (
|
|
37
|
+
if (cloudKey) headers.Authorization = `Bearer ${cloudKey}`;
|
|
39
38
|
|
|
40
39
|
ctx.agent.providers.configure(id, {
|
|
41
|
-
reasoningParams: (level) =>
|
|
40
|
+
reasoningParams: (level) => {
|
|
41
|
+
if (level === "off") return { reasoning_effort: "none" };
|
|
42
|
+
return { reasoning_effort: level === "xhigh" ? "high" : level };
|
|
43
|
+
},
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
ctx.bus.emit("provider:register", { id, apiKey: sdkKey, baseURL, models: [] });
|
|
@@ -19,7 +19,8 @@ const DEFAULT_MODELS = [
|
|
|
19
19
|
|
|
20
20
|
function buildReasoningParams(level: string, _model?: string): Record<string, unknown> {
|
|
21
21
|
if (level === "off") return { thinking: { type: "disabled" } };
|
|
22
|
-
|
|
22
|
+
const effort = level === "xhigh" ? "high" : level;
|
|
23
|
+
return { thinking: { type: "enabled" }, reasoning_effort: effort };
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export default function activate(ctx: AgentContext): void {
|
package/package.json
CHANGED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ollama Cloud — hosted Ollama models (https://ollama.com).
|
|
3
|
-
*
|
|
4
|
-
* Auth: agent-sh auth login ollama-cloud
|
|
5
|
-
* Usage: agent-sh -e ./examples/extensions/ollama-cloud.ts
|
|
6
|
-
*/
|
|
7
|
-
import { resolveApiKey } from "agent-sh/auth";
|
|
8
|
-
import type { AgentContext } from "agent-sh/types";
|
|
9
|
-
|
|
10
|
-
const HOST = "https://ollama.com";
|
|
11
|
-
const BASE_URL = `${HOST}/v1`;
|
|
12
|
-
const ID = "ollama-cloud";
|
|
13
|
-
|
|
14
|
-
function buildReasoningParams(level: string, _model?: string): Record<string, unknown> {
|
|
15
|
-
return { reasoning_effort: level === "off" ? "none" : level };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function fetchModels(apiKey: string) {
|
|
19
|
-
const headers: Record<string, string> = { Authorization: `Bearer ${apiKey}` };
|
|
20
|
-
const tagsRes = await fetch(`${HOST}/api/tags`, { headers });
|
|
21
|
-
if (!tagsRes.ok) return [];
|
|
22
|
-
const tagsData = await tagsRes.json() as { models?: { name: string }[] };
|
|
23
|
-
const names = (tagsData.models ?? []).map((m) => m.name);
|
|
24
|
-
if (!names.length) return [];
|
|
25
|
-
|
|
26
|
-
const ctxs = await Promise.all(
|
|
27
|
-
names.map((name) =>
|
|
28
|
-
fetch(`${HOST}/api/show`, {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
31
|
-
body: JSON.stringify({ name }),
|
|
32
|
-
})
|
|
33
|
-
.then((r) => r.ok ? r.json() as Promise<{ model_info?: Record<string, unknown> }> : null)
|
|
34
|
-
.then((d) => {
|
|
35
|
-
if (!d?.model_info) return undefined;
|
|
36
|
-
const info = d.model_info;
|
|
37
|
-
const arch = info["general.architecture"] as string | undefined;
|
|
38
|
-
if (arch) {
|
|
39
|
-
const ctx = info[`${arch}.context_length`];
|
|
40
|
-
if (typeof ctx === "number") return ctx;
|
|
41
|
-
}
|
|
42
|
-
for (const [k, v] of Object.entries(info)) {
|
|
43
|
-
if (k.endsWith(".context_length") && typeof v === "number") return v;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
})
|
|
47
|
-
.catch(() => undefined),
|
|
48
|
-
),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
return names.map((name, i) => ({
|
|
52
|
-
id: name,
|
|
53
|
-
contextWindow: ctxs[i],
|
|
54
|
-
echoReasoning: /deepseek/i.test(name),
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export default function activate(ctx: AgentContext): void {
|
|
59
|
-
const { key } = resolveApiKey(ID);
|
|
60
|
-
const apiKey = key ?? process.env.OLLAMA_API_KEY;
|
|
61
|
-
if (!apiKey) return;
|
|
62
|
-
|
|
63
|
-
ctx.agent.providers.configure(ID, { reasoningParams: buildReasoningParams });
|
|
64
|
-
|
|
65
|
-
// Register placeholder while catalog loads
|
|
66
|
-
ctx.bus.emit("provider:register", { id: ID, apiKey, baseURL: BASE_URL, models: [] });
|
|
67
|
-
|
|
68
|
-
fetchModels(apiKey).then((models) => {
|
|
69
|
-
if (!models.length) return;
|
|
70
|
-
ctx.bus.emit("provider:register", {
|
|
71
|
-
id: ID,
|
|
72
|
-
apiKey,
|
|
73
|
-
baseURL: BASE_URL,
|
|
74
|
-
defaultModel: models[0]!.id,
|
|
75
|
-
models,
|
|
76
|
-
});
|
|
77
|
-
}).catch(() => { /* keep placeholder */ });
|
|
78
|
-
}
|