min-agent 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/agent.js +36 -22
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +22 -5
- package/dist/cli/commands/memory.js +33 -15
- package/dist/cli/commands/think.js +12 -0
- package/dist/cli/commands/write-config.js +22 -0
- package/dist/cli/option-helpers.js +13 -1
- package/dist/cli/program.js +50 -13
- package/dist/code-mode.js +1 -1
- package/dist/config.js +41 -0
- package/dist/context-window.js +8 -28
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -0
- package/dist/permission-cli.js +1 -4
- package/dist/provider.js +4 -1
- package/dist/reasoning-stream.js +158 -0
- package/dist/sandbox-cli.js +1 -4
- package/dist/scope.js +23 -0
- package/dist/serve/common.js +22 -1
- package/dist/serve/routes-chat.js +21 -1
- package/dist/serve/routes-memory.js +31 -2
- package/dist/serve/routes-meta.js +34 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +228 -0
- package/dist/thinking.js +142 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +36 -8
- package/dist/tui/InputBar.js +109 -36
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +77 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +7 -1
- package/dist/tui/layout.js +17 -0
- package/dist/tui/overlay-input.js +12 -0
- package/dist/tui/paste-draft.js +173 -0
- package/dist/tui/selection.js +8 -2
- package/dist/tui/slash-commands.js +18 -1
- package/dist/tui/slash-handler.js +61 -17
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +63 -7
- package/docs/API.md +50 -4
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +5 -4
- package/skills/self-config/reference.md +10 -5
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
4
|
+
import { getConfigDir, parseThinkingEffort } from "./config.js";
|
|
5
|
+
const DEFAULT_LONAE_BASE = "https://models.lonae.com";
|
|
6
|
+
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
7
|
+
const CATALOG_ENTRY_VERSION = 3;
|
|
8
|
+
const NEGATIVE_TTL = 60 * 60 * 1000;
|
|
9
|
+
const FETCH_TIMEOUT_MS = 10000;
|
|
10
|
+
const memoryCache = new Map();
|
|
11
|
+
const inFlight = new Map();
|
|
12
|
+
const negativeCache = new Map();
|
|
13
|
+
export function lonaeBaseUrl() {
|
|
14
|
+
const raw = process.env.MIN_AGENT_MODELS_API_URL?.trim();
|
|
15
|
+
if (!raw)
|
|
16
|
+
return DEFAULT_LONAE_BASE;
|
|
17
|
+
return raw.replace(/\/$/, "");
|
|
18
|
+
}
|
|
19
|
+
function cacheFile() {
|
|
20
|
+
return path.join(getConfigDir(), "model-catalog-cache.json");
|
|
21
|
+
}
|
|
22
|
+
function loadDiskCache() {
|
|
23
|
+
const file = cacheFile();
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return {};
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function saveDiskCache(cache) {
|
|
34
|
+
const file = cacheFile();
|
|
35
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
+
atomicWriteFileSync(file, JSON.stringify(cache));
|
|
37
|
+
}
|
|
38
|
+
export function clearModelCatalogCache() {
|
|
39
|
+
memoryCache.clear();
|
|
40
|
+
inFlight.clear();
|
|
41
|
+
negativeCache.clear();
|
|
42
|
+
}
|
|
43
|
+
export function getCachedModelCatalog(modelId) {
|
|
44
|
+
const memory = memoryCache.get(modelId);
|
|
45
|
+
if (memory && memory.version === CATALOG_ENTRY_VERSION && Date.now() - memory.timestamp <= CACHE_TTL)
|
|
46
|
+
return memory;
|
|
47
|
+
const disk = loadDiskCache()[modelId];
|
|
48
|
+
if (!disk || disk.version !== CATALOG_ENTRY_VERSION || Date.now() - disk.timestamp > CACHE_TTL)
|
|
49
|
+
return null;
|
|
50
|
+
memoryCache.set(modelId, disk);
|
|
51
|
+
return disk;
|
|
52
|
+
}
|
|
53
|
+
function setCache(modelId, entry) {
|
|
54
|
+
memoryCache.set(modelId, entry);
|
|
55
|
+
const cache = loadDiskCache();
|
|
56
|
+
cache[modelId] = entry;
|
|
57
|
+
saveDiskCache(cache);
|
|
58
|
+
}
|
|
59
|
+
function isRecord(value) {
|
|
60
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
export function parseReasoningOptions(raw) {
|
|
63
|
+
if (!Array.isArray(raw))
|
|
64
|
+
return { toggle: false, efforts: [] };
|
|
65
|
+
let toggle = false;
|
|
66
|
+
const efforts = new Set();
|
|
67
|
+
for (const item of raw) {
|
|
68
|
+
if (!isRecord(item))
|
|
69
|
+
continue;
|
|
70
|
+
if (item.type === "toggle")
|
|
71
|
+
toggle = true;
|
|
72
|
+
if (item.type === "effort" && Array.isArray(item.values)) {
|
|
73
|
+
for (const value of item.values) {
|
|
74
|
+
if (typeof value === "string" && value && value !== "default")
|
|
75
|
+
efforts.add(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (efforts.has("none"))
|
|
80
|
+
toggle = true;
|
|
81
|
+
return { toggle, efforts: [...efforts] };
|
|
82
|
+
}
|
|
83
|
+
export function pickListedModel(models, modelId) {
|
|
84
|
+
const lower = modelId.toLowerCase();
|
|
85
|
+
const exact = models.find((m) => m.id.toLowerCase() === lower);
|
|
86
|
+
if (exact)
|
|
87
|
+
return exact;
|
|
88
|
+
const suffix = models.find((m) => {
|
|
89
|
+
const id = m.id.toLowerCase();
|
|
90
|
+
return id.endsWith(`/${lower}`) || lower.endsWith(`/${id}`);
|
|
91
|
+
});
|
|
92
|
+
if (suffix)
|
|
93
|
+
return suffix;
|
|
94
|
+
return models.find((m) => m.name?.toLowerCase() === lower);
|
|
95
|
+
}
|
|
96
|
+
export function matchOffering(offerings, baseURL) {
|
|
97
|
+
if (!baseURL || offerings.length === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
let host = "";
|
|
100
|
+
try {
|
|
101
|
+
host = new URL(baseURL).hostname.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return offerings.find((o) => {
|
|
107
|
+
const id = String(o.providerId ?? "")
|
|
108
|
+
.toLowerCase()
|
|
109
|
+
.replace(/_/g, "-");
|
|
110
|
+
if (!id)
|
|
111
|
+
return false;
|
|
112
|
+
if (host.includes(id))
|
|
113
|
+
return true;
|
|
114
|
+
const slug = id.split("-")[0] ?? "";
|
|
115
|
+
return slug.length >= 4 && host.includes(slug);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const CANONICAL_EFFORTS = ["low", "medium", "high", "max"];
|
|
119
|
+
function hasEffortLevel(parsed) {
|
|
120
|
+
return parsed.efforts.some((raw) => {
|
|
121
|
+
const canonical = parseThinkingEffort(raw);
|
|
122
|
+
return Boolean(canonical && canonical !== "off");
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** When the active host is unknown, keep levels a majority of offerings actually list. */
|
|
126
|
+
export function majorityReasoning(offerings) {
|
|
127
|
+
const voters = offerings
|
|
128
|
+
.map((offering) => parseReasoningOptions(offering.reasoningOptions))
|
|
129
|
+
.filter((parsed) => parsed.toggle || parsed.efforts.length > 0);
|
|
130
|
+
if (voters.length === 0)
|
|
131
|
+
return { toggle: false, efforts: [] };
|
|
132
|
+
const effortVoters = voters.filter(hasEffortLevel);
|
|
133
|
+
const toggleThreshold = voters.length / 2;
|
|
134
|
+
const effortThreshold = effortVoters.length === 0 ? Number.POSITIVE_INFINITY : effortVoters.length / 2;
|
|
135
|
+
let toggleVotes = 0;
|
|
136
|
+
let xhighVotes = 0;
|
|
137
|
+
const votes = new Map();
|
|
138
|
+
for (const parsed of voters) {
|
|
139
|
+
if (parsed.toggle || parsed.efforts.includes("none"))
|
|
140
|
+
toggleVotes++;
|
|
141
|
+
}
|
|
142
|
+
for (const parsed of effortVoters) {
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
for (const raw of parsed.efforts) {
|
|
145
|
+
if (raw === "xhigh" || raw === "extra-high")
|
|
146
|
+
xhighVotes++;
|
|
147
|
+
const canonical = parseThinkingEffort(raw);
|
|
148
|
+
if (!canonical || canonical === "off" || seen.has(canonical))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(canonical);
|
|
151
|
+
votes.set(canonical, (votes.get(canonical) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const toggle = toggleVotes >= toggleThreshold;
|
|
155
|
+
const efforts = [];
|
|
156
|
+
if (toggle)
|
|
157
|
+
efforts.push("none");
|
|
158
|
+
for (const key of CANONICAL_EFFORTS) {
|
|
159
|
+
if ((votes.get(key) ?? 0) >= effortThreshold)
|
|
160
|
+
efforts.push(key);
|
|
161
|
+
}
|
|
162
|
+
if (efforts.includes("max") && xhighVotes > 0)
|
|
163
|
+
efforts.push("xhigh");
|
|
164
|
+
return { toggle, efforts };
|
|
165
|
+
}
|
|
166
|
+
function contextFrom(limit) {
|
|
167
|
+
const n = limit?.context;
|
|
168
|
+
if (typeof n === "number" && Number.isFinite(n) && n >= 4096)
|
|
169
|
+
return Math.floor(n);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
export function catalogFromLonaeModel(model, modelId, baseURL) {
|
|
173
|
+
const offerings = model.offerings ?? [];
|
|
174
|
+
const matched = matchOffering(offerings, baseURL);
|
|
175
|
+
const reasoning = matched ? parseReasoningOptions(matched.reasoningOptions) : majorityReasoning(offerings);
|
|
176
|
+
const contextWindow = contextFrom(matched?.limit) ?? contextFrom(model.limit);
|
|
177
|
+
return {
|
|
178
|
+
version: CATALOG_ENTRY_VERSION,
|
|
179
|
+
modelId,
|
|
180
|
+
lonaeId: model.id,
|
|
181
|
+
...(contextWindow != null ? { contextWindow } : {}),
|
|
182
|
+
reasoning,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function fetchJson(url) {
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
189
|
+
if (!response.ok)
|
|
190
|
+
return null;
|
|
191
|
+
return await response.json();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function readModel(payload) {
|
|
198
|
+
const root = isRecord(payload) ? payload : null;
|
|
199
|
+
const data = root && isRecord(root.data) ? root.data : root;
|
|
200
|
+
if (!data || typeof data.id !== "string")
|
|
201
|
+
return null;
|
|
202
|
+
const limit = isRecord(data.limit)
|
|
203
|
+
? { context: typeof data.limit.context === "number" ? data.limit.context : undefined }
|
|
204
|
+
: undefined;
|
|
205
|
+
const offerings = Array.isArray(data.offerings)
|
|
206
|
+
? data.offerings.filter(isRecord).map((o) => ({
|
|
207
|
+
providerId: typeof o.providerId === "string" ? o.providerId : undefined,
|
|
208
|
+
limit: isRecord(o.limit)
|
|
209
|
+
? { context: typeof o.limit.context === "number" ? o.limit.context : undefined }
|
|
210
|
+
: undefined,
|
|
211
|
+
reasoningOptions: o.reasoningOptions,
|
|
212
|
+
}))
|
|
213
|
+
: [];
|
|
214
|
+
return {
|
|
215
|
+
id: data.id,
|
|
216
|
+
limit,
|
|
217
|
+
reasoning: data.reasoning === true,
|
|
218
|
+
offerings,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function readList(payload) {
|
|
222
|
+
const root = isRecord(payload) ? payload : null;
|
|
223
|
+
const data = root && Array.isArray(root.data) ? root.data : Array.isArray(payload) ? payload : [];
|
|
224
|
+
return data.flatMap((item) => {
|
|
225
|
+
if (!isRecord(item) || typeof item.id !== "string")
|
|
226
|
+
return [];
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
id: item.id,
|
|
230
|
+
name: typeof item.name === "string" ? item.name : undefined,
|
|
231
|
+
context: typeof item.context === "number" ? item.context : undefined,
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function modelPath(id) {
|
|
237
|
+
return id
|
|
238
|
+
.split("/")
|
|
239
|
+
.map((part) => encodeURIComponent(part))
|
|
240
|
+
.join("/");
|
|
241
|
+
}
|
|
242
|
+
async function fetchLonaeModel(modelId) {
|
|
243
|
+
const base = lonaeBaseUrl();
|
|
244
|
+
if (modelId.includes("/")) {
|
|
245
|
+
const direct = readModel(await fetchJson(`${base}/api/v1/models/${modelPath(modelId)}`));
|
|
246
|
+
if (direct)
|
|
247
|
+
return direct;
|
|
248
|
+
}
|
|
249
|
+
const listed = readList(await fetchJson(`${base}/api/v1/models?q=${encodeURIComponent(modelId)}&page_size=20`));
|
|
250
|
+
const match = pickListedModel(listed, modelId);
|
|
251
|
+
if (!match)
|
|
252
|
+
return null;
|
|
253
|
+
return readModel(await fetchJson(`${base}/api/v1/models/${modelPath(match.id)}`));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Resolve context window and thinking options from the Lonae catalog.
|
|
257
|
+
* Successful lookups are cached in memory and on disk for 7 days.
|
|
258
|
+
*/
|
|
259
|
+
export async function getModelCatalog(modelId, baseURL) {
|
|
260
|
+
const cached = getCachedModelCatalog(modelId);
|
|
261
|
+
if (cached)
|
|
262
|
+
return cached;
|
|
263
|
+
const negAt = negativeCache.get(modelId);
|
|
264
|
+
if (negAt !== undefined && Date.now() - negAt <= NEGATIVE_TTL)
|
|
265
|
+
return null;
|
|
266
|
+
const pending = inFlight.get(modelId);
|
|
267
|
+
if (pending)
|
|
268
|
+
return pending;
|
|
269
|
+
const probing = fetchLonaeModel(modelId).then((model) => {
|
|
270
|
+
if (!model) {
|
|
271
|
+
negativeCache.set(modelId, Date.now());
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const entry = catalogFromLonaeModel(model, modelId, baseURL);
|
|
275
|
+
setCache(modelId, entry);
|
|
276
|
+
return entry;
|
|
277
|
+
});
|
|
278
|
+
inFlight.set(modelId, probing);
|
|
279
|
+
try {
|
|
280
|
+
return await probing;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
inFlight.delete(modelId);
|
|
284
|
+
}
|
|
285
|
+
}
|
package/dist/permission-cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { takeScopeFlags } from "./
|
|
1
|
+
import { takeScopeFlags, scopeLabel } from "./scope.js";
|
|
2
2
|
import { getPermissionSnapshot, parsePermissionMode, permissionModeLabel, setPermissionMode, } from "./config.js";
|
|
3
3
|
import { getPermissionOverride, setPermissionOverride } from "./confirm.js";
|
|
4
4
|
export const PERMISSION_CLI_USAGE = [
|
|
@@ -8,9 +8,6 @@ export const PERMISSION_CLI_USAGE = [
|
|
|
8
8
|
function sourceLabel(source) {
|
|
9
9
|
return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
|
|
10
10
|
}
|
|
11
|
-
function scopeLabel(scope) {
|
|
12
|
-
return scope === "project" ? "project" : "global";
|
|
13
|
-
}
|
|
14
11
|
export function runPermissionCli(input) {
|
|
15
12
|
const { scope: posScope, rest } = takeScopeFlags(input.positionals);
|
|
16
13
|
const scope = posScope ?? input.scope ?? "global";
|
package/dist/provider.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
2
|
import { getEffectiveConfig, getActiveProvider, normalizeOllamaBaseURL } from "./config.js";
|
|
3
3
|
import { createTimeoutFetch, DEFAULT_FIRST_BYTE_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, } from "./fetch-timeout.js";
|
|
4
|
+
import { foldReasoningInChatResponse } from "./reasoning-stream.js";
|
|
5
|
+
import { fetchWithThinkingWire } from "./thinking-wire.js";
|
|
4
6
|
function positiveMs(raw, fallback) {
|
|
5
7
|
const value = typeof raw === "string" ? Number(raw) : raw;
|
|
6
8
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
@@ -10,11 +12,12 @@ function positiveMs(raw, fallback) {
|
|
|
10
12
|
/** Timeout/trace-aware fetch shared by every provider client. */
|
|
11
13
|
export function modelFetch() {
|
|
12
14
|
const cfg = getEffectiveConfig();
|
|
13
|
-
|
|
15
|
+
const inner = createTimeoutFetch({
|
|
14
16
|
firstByteTimeoutMs: positiveMs(process.env.MIN_AGENT_REQUEST_TIMEOUT_MS ?? cfg.agent?.requestTimeoutMs, DEFAULT_FIRST_BYTE_TIMEOUT_MS),
|
|
15
17
|
idleTimeoutMs: positiveMs(process.env.MIN_AGENT_STREAM_IDLE_TIMEOUT_MS ?? cfg.agent?.streamIdleTimeoutMs, DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
16
18
|
trace: process.env.MIN_AGENT_TRACE === "1" || process.env.MIN_AGENT_TRACE === "true",
|
|
17
19
|
});
|
|
20
|
+
return (input, init) => fetchWithThinkingWire(inner, input, init).then(foldReasoningInChatResponse);
|
|
18
21
|
}
|
|
19
22
|
export function resolveModelForProvider(provider, modelId) {
|
|
20
23
|
if (!provider.baseURL || !provider.apiKey) {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible chat completions put the thinking chain in
|
|
3
|
+
* `reasoning_content` / `reasoning` / `reasoning_details`. The @ai-sdk/openai
|
|
4
|
+
* chat parser only reads `delta.content`, so those fields never become
|
|
5
|
+
* reasoning events. Fold them into `<think>` tags that ThinkingBodySplitter
|
|
6
|
+
* already turns into on-screen thinking.
|
|
7
|
+
*/
|
|
8
|
+
function isRecord(v) {
|
|
9
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
|
+
}
|
|
11
|
+
function sanitizeReasoning(text) {
|
|
12
|
+
return text.replace(/<\/?(?:think|thinking)>/gi, "");
|
|
13
|
+
}
|
|
14
|
+
function stringField(v, key) {
|
|
15
|
+
if (!isRecord(v))
|
|
16
|
+
return "";
|
|
17
|
+
return typeof v[key] === "string" ? v[key] : "";
|
|
18
|
+
}
|
|
19
|
+
function reasoningFromDetails(details) {
|
|
20
|
+
if (!Array.isArray(details))
|
|
21
|
+
return "";
|
|
22
|
+
return details
|
|
23
|
+
.map((item) => {
|
|
24
|
+
const text = stringField(item, "text");
|
|
25
|
+
if (text)
|
|
26
|
+
return text;
|
|
27
|
+
return stringField(item, "content");
|
|
28
|
+
})
|
|
29
|
+
.join("");
|
|
30
|
+
}
|
|
31
|
+
function reasoningFromValue(v) {
|
|
32
|
+
if (typeof v === "string")
|
|
33
|
+
return v;
|
|
34
|
+
if (!isRecord(v))
|
|
35
|
+
return "";
|
|
36
|
+
return stringField(v, "content") || stringField(v, "text");
|
|
37
|
+
}
|
|
38
|
+
export function reasoningTextFromDelta(delta) {
|
|
39
|
+
const details = reasoningFromDetails(delta.reasoning_details);
|
|
40
|
+
const primary = reasoningFromValue(delta.reasoning_content) ||
|
|
41
|
+
reasoningFromValue(delta.reasoning) ||
|
|
42
|
+
reasoningFromValue(delta.thinking);
|
|
43
|
+
return sanitizeReasoning(`${details}${primary}`);
|
|
44
|
+
}
|
|
45
|
+
export function createReasoningDeltaTracker() {
|
|
46
|
+
let acc = "";
|
|
47
|
+
return (text) => {
|
|
48
|
+
if (!text)
|
|
49
|
+
return "";
|
|
50
|
+
if (acc && text.startsWith(acc)) {
|
|
51
|
+
const extra = text.slice(acc.length);
|
|
52
|
+
acc = text;
|
|
53
|
+
return extra;
|
|
54
|
+
}
|
|
55
|
+
acc += text;
|
|
56
|
+
return text;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function foldReasoningIntoDelta(delta, takeDelta) {
|
|
60
|
+
const reasoning = takeDelta(reasoningTextFromDelta(delta));
|
|
61
|
+
if (!reasoning)
|
|
62
|
+
return false;
|
|
63
|
+
const content = typeof delta.content === "string" ? delta.content : "";
|
|
64
|
+
delta.content = `<think>${reasoning}</think>${content}`;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
export function foldReasoningInChunk(chunk, takeDelta = (t) => t) {
|
|
68
|
+
if (!isRecord(chunk) || !Array.isArray(chunk.choices))
|
|
69
|
+
return false;
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const choice of chunk.choices) {
|
|
72
|
+
if (!isRecord(choice))
|
|
73
|
+
continue;
|
|
74
|
+
if (isRecord(choice.delta) && foldReasoningIntoDelta(choice.delta, takeDelta))
|
|
75
|
+
changed = true;
|
|
76
|
+
if (isRecord(choice.message) && foldReasoningIntoDelta(choice.message, takeDelta))
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
return changed;
|
|
80
|
+
}
|
|
81
|
+
export function foldReasoningIntoSseLine(line, takeDelta) {
|
|
82
|
+
const trimmed = line.replace(/\r$/, "");
|
|
83
|
+
if (!trimmed.startsWith("data:"))
|
|
84
|
+
return line;
|
|
85
|
+
const payload = trimmed.slice("data:".length).trim();
|
|
86
|
+
if (!payload || payload === "[DONE]")
|
|
87
|
+
return line;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(payload);
|
|
90
|
+
if (!foldReasoningInChunk(parsed, takeDelta))
|
|
91
|
+
return line;
|
|
92
|
+
const prefix = line.startsWith("data: ") ? "data: " : "data:";
|
|
93
|
+
const nl = line.endsWith("\r") ? "\r" : "";
|
|
94
|
+
return `${prefix}${JSON.stringify(parsed)}${nl}`;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return line;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function rewriteSseFragment(fragment, takeDelta) {
|
|
101
|
+
const keepLastNl = fragment.endsWith("\n");
|
|
102
|
+
const raw = keepLastNl ? fragment.slice(0, -1) : fragment;
|
|
103
|
+
return (raw
|
|
104
|
+
.split("\n")
|
|
105
|
+
.map((line) => foldReasoningIntoSseLine(line, takeDelta))
|
|
106
|
+
.join("\n") + (keepLastNl ? "\n" : ""));
|
|
107
|
+
}
|
|
108
|
+
export function transformReasoningSse(source) {
|
|
109
|
+
const decoder = new TextDecoder();
|
|
110
|
+
const encoder = new TextEncoder();
|
|
111
|
+
const reader = source.getReader();
|
|
112
|
+
const takeDelta = createReasoningDeltaTracker();
|
|
113
|
+
let pending = "";
|
|
114
|
+
return new ReadableStream({
|
|
115
|
+
async pull(controller) {
|
|
116
|
+
for (;;) {
|
|
117
|
+
const { done, value } = await reader.read();
|
|
118
|
+
if (done) {
|
|
119
|
+
pending += decoder.decode();
|
|
120
|
+
if (pending)
|
|
121
|
+
controller.enqueue(encoder.encode(rewriteSseFragment(pending, takeDelta)));
|
|
122
|
+
controller.close();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
pending += decoder.decode(value, { stream: true });
|
|
126
|
+
const nl = pending.lastIndexOf("\n");
|
|
127
|
+
if (nl < 0)
|
|
128
|
+
continue;
|
|
129
|
+
const complete = pending.slice(0, nl + 1);
|
|
130
|
+
pending = pending.slice(nl + 1);
|
|
131
|
+
controller.enqueue(encoder.encode(rewriteSseFragment(complete, takeDelta)));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
cancel(reason) {
|
|
136
|
+
return reader.cancel(reason);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function isChatSseResponse(response) {
|
|
141
|
+
const ctype = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
142
|
+
if (ctype.includes("event-stream"))
|
|
143
|
+
return true;
|
|
144
|
+
if (!ctype || ctype.startsWith("text/plain"))
|
|
145
|
+
return true;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
export function foldReasoningInChatResponse(response) {
|
|
149
|
+
if (!response.body || response.status >= 400)
|
|
150
|
+
return response;
|
|
151
|
+
if (!isChatSseResponse(response))
|
|
152
|
+
return response;
|
|
153
|
+
return new Response(transformReasoningSse(response.body), {
|
|
154
|
+
status: response.status,
|
|
155
|
+
statusText: response.statusText,
|
|
156
|
+
headers: response.headers,
|
|
157
|
+
});
|
|
158
|
+
}
|
package/dist/sandbox-cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { takeScopeFlags } from "./
|
|
1
|
+
import { takeScopeFlags, scopeLabel } from "./scope.js";
|
|
2
2
|
import { getSandboxSnapshot, setSandboxConfig } from "./config.js";
|
|
3
3
|
import { parseNetworkPolicy, parseSandboxMode, getEffectiveSandboxPolicy, sandboxEnforcementCaveat, sandboxModeLabel, sandboxStatusLabel, setSandboxOverride, } from "./sandbox.js";
|
|
4
4
|
export const SANDBOX_CLI_USAGE = [
|
|
@@ -17,9 +17,6 @@ function sourceLabel(source) {
|
|
|
17
17
|
? "global"
|
|
18
18
|
: "default";
|
|
19
19
|
}
|
|
20
|
-
function scopeLabel(scope) {
|
|
21
|
-
return scope === "project" ? "project" : "global";
|
|
22
|
-
}
|
|
23
20
|
export function runSandboxCli(input) {
|
|
24
21
|
const { scope: posScope, rest } = takeScopeFlags(input.positionals);
|
|
25
22
|
const scope = posScope ?? input.scope ?? "global";
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function readScope(value) {
|
|
2
|
+
if (value === undefined || value === null || value === "")
|
|
3
|
+
return { ok: true, scope: undefined };
|
|
4
|
+
if (value === "global" || value === "project")
|
|
5
|
+
return { ok: true, scope: value };
|
|
6
|
+
return { ok: false };
|
|
7
|
+
}
|
|
8
|
+
export function takeScopeFlags(tokens) {
|
|
9
|
+
let scope;
|
|
10
|
+
const rest = [];
|
|
11
|
+
for (const t of tokens) {
|
|
12
|
+
if (t === "--project")
|
|
13
|
+
scope = "project";
|
|
14
|
+
else if (t === "--global")
|
|
15
|
+
scope = "global";
|
|
16
|
+
else
|
|
17
|
+
rest.push(t);
|
|
18
|
+
}
|
|
19
|
+
return { scope, rest };
|
|
20
|
+
}
|
|
21
|
+
export function scopeLabel(scope) {
|
|
22
|
+
return scope === "project" ? "project" : "global";
|
|
23
|
+
}
|
package/dist/serve/common.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { getSandboxSnapshot, getPermissionSnapshot, permissionModeLabel } from "../config.js";
|
|
2
|
+
import { getSandboxSnapshot, getPermissionSnapshot, permissionModeLabel, parseThinkingEffort, parseMemoryMode, } from "../config.js";
|
|
3
3
|
import { parseNetworkPolicy, parseSandboxMode, runWithSandboxPolicy } from "../sandbox.js";
|
|
4
4
|
import { runWithConfirmContext, getPermissionOverride } from "../confirm.js";
|
|
5
|
+
import { thinkingPayload } from "../thinking.js";
|
|
6
|
+
import { memoryPayload } from "../memory.js";
|
|
5
7
|
import { runOnceWithSystem } from "../agent.js";
|
|
6
8
|
import { saveSession, sanitizeForStorage } from "../sessions.js";
|
|
7
9
|
import { createSaveThrottle } from "../save-throttle.js";
|
|
@@ -266,6 +268,23 @@ export function permissionPayload() {
|
|
|
266
268
|
label: permissionModeLabel(permission),
|
|
267
269
|
};
|
|
268
270
|
}
|
|
271
|
+
export { thinkingPayload, memoryPayload };
|
|
272
|
+
export function readThinking(body) {
|
|
273
|
+
if (body.thinking === undefined)
|
|
274
|
+
return { ok: true };
|
|
275
|
+
const thinking = parseThinkingEffort(body.thinking);
|
|
276
|
+
if (!thinking)
|
|
277
|
+
return { ok: false, error: "invalid_thinking", detail: "thinking must be off, low, medium, high, or max" };
|
|
278
|
+
return { ok: true, thinking };
|
|
279
|
+
}
|
|
280
|
+
export function readMemory(body) {
|
|
281
|
+
if (body.memory === undefined)
|
|
282
|
+
return { ok: true };
|
|
283
|
+
const memory = parseMemoryMode(body.memory);
|
|
284
|
+
if (!memory)
|
|
285
|
+
return { ok: false, error: "invalid_memory", detail: "memory must be on or off" };
|
|
286
|
+
return { ok: true, memory };
|
|
287
|
+
}
|
|
269
288
|
export function normalizeMessages(body) {
|
|
270
289
|
if (body.messages && Array.isArray(body.messages)) {
|
|
271
290
|
if (body.messages.length === 0)
|
|
@@ -282,6 +301,8 @@ export function chatRunOptions(body) {
|
|
|
282
301
|
temperature: typeof body.temperature === "number" ? body.temperature : undefined,
|
|
283
302
|
maxTokens: typeof body.maxTokens === "number" ? body.maxTokens : undefined,
|
|
284
303
|
topP: typeof body.topP === "number" ? body.topP : undefined,
|
|
304
|
+
thinking: parseThinkingEffort(body.thinking),
|
|
305
|
+
memory: parseMemoryMode(body.memory),
|
|
285
306
|
providerName: typeof body.provider === "string" ? body.provider : undefined,
|
|
286
307
|
...(body.plan_mode === true ? { planMode: true } : {}),
|
|
287
308
|
};
|
|
@@ -5,7 +5,7 @@ import { lastUserText } from "../tui/hydrate.js";
|
|
|
5
5
|
import { compactMessages } from "../compaction.js";
|
|
6
6
|
import { resolveModel } from "../provider.js";
|
|
7
7
|
import { emptyTaskState } from "../tools/todo.js";
|
|
8
|
-
import { MAX_CONCURRENT_CHATS, sendJson, sendJsonError, readJsonBody, readSandboxTighten, normalizeMessages, chatRunOptions, executeChat, } from "./common.js";
|
|
8
|
+
import { MAX_CONCURRENT_CHATS, sendJson, sendJsonError, readJsonBody, readSandboxTighten, readThinking, readMemory, normalizeMessages, chatRunOptions, executeChat, } from "./common.js";
|
|
9
9
|
/** Shared handler for /v1/chat and /v1/code (streaming + JSON). */
|
|
10
10
|
async function handleChatRequest(ctx, req, res) {
|
|
11
11
|
if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
|
|
@@ -33,6 +33,16 @@ async function handleChatRequest(ctx, req, res) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const sandboxTighten = sandboxParsed.policy;
|
|
36
|
+
const thinkingParsed = readThinking(body);
|
|
37
|
+
if (!thinkingParsed.ok) {
|
|
38
|
+
sendJson(res, 400, { error: thinkingParsed.error, detail: thinkingParsed.detail });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const memoryParsed = readMemory(body);
|
|
42
|
+
if (!memoryParsed.ok) {
|
|
43
|
+
sendJson(res, 400, { error: memoryParsed.error, detail: memoryParsed.detail });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
36
46
|
const modelId = typeof body.model === "string" ? body.model : undefined;
|
|
37
47
|
const stream = body.stream === true;
|
|
38
48
|
const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
|
|
@@ -132,6 +142,11 @@ export const handleChatRoutes = async (req, res, ctx, _url, pathname) => {
|
|
|
132
142
|
sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
|
|
133
143
|
return true;
|
|
134
144
|
}
|
|
145
|
+
const thinkingParsed = readThinking(body);
|
|
146
|
+
if (!thinkingParsed.ok) {
|
|
147
|
+
sendJson(res, 400, { error: thinkingParsed.error, detail: thinkingParsed.detail });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
135
150
|
if (ctx.activeChats >= MAX_CONCURRENT_CHATS) {
|
|
136
151
|
sendJson(res, 429, {
|
|
137
152
|
error: "too_many_concurrent_requests",
|
|
@@ -242,6 +257,11 @@ export const handleChatRoutes = async (req, res, ctx, _url, pathname) => {
|
|
|
242
257
|
sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
|
|
243
258
|
return true;
|
|
244
259
|
}
|
|
260
|
+
const thinkingParsed = readThinking(body);
|
|
261
|
+
if (!thinkingParsed.ok) {
|
|
262
|
+
sendJson(res, 400, { error: thinkingParsed.error, detail: thinkingParsed.detail });
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
245
265
|
const imageBuffer = Buffer.from(body.image_base64, "base64");
|
|
246
266
|
const mimeType = body.mime_type ?? "image/png";
|
|
247
267
|
const text = body.message ?? "What's in this image?";
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
-
import { loadMemories, addMemory, deleteMemory, searchMemories, readMemoryScope } from "../memory.js";
|
|
1
|
+
import { loadMemories, addMemory, deleteMemory, searchMemories, readMemoryScope, parseMemoryMode, setMemoryMode, memoryPayload, } from "../memory.js";
|
|
2
2
|
import { sendJson, sendJsonError, readJsonBody } from "./common.js";
|
|
3
3
|
export const handleMemoryRoutes = async (req, res, _ctx, url, pathname) => {
|
|
4
|
+
if (req.method === "GET" && pathname === "/v1/memory/mode") {
|
|
5
|
+
sendJson(res, 200, memoryPayload());
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (req.method === "POST" && pathname === "/v1/memory/mode") {
|
|
9
|
+
const parsed = await readJsonBody(req);
|
|
10
|
+
if (!parsed.ok) {
|
|
11
|
+
sendJsonError(res, parsed);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
const mode = parseMemoryMode(parsed.value.memory);
|
|
15
|
+
if (!mode) {
|
|
16
|
+
sendJson(res, 400, { error: "invalid_memory", detail: "memory must be on or off" });
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const scopeParsed = readMemoryScope(parsed.value.scope);
|
|
20
|
+
if (!scopeParsed.ok) {
|
|
21
|
+
sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const scope = scopeParsed.scope ?? "global";
|
|
25
|
+
setMemoryMode(mode, scope);
|
|
26
|
+
sendJson(res, 200, { ok: true, scope, ...memoryPayload() });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
4
29
|
if (req.method === "GET" && pathname === "/v1/memory") {
|
|
5
|
-
sendJson(res, 200, {
|
|
30
|
+
sendJson(res, 200, {
|
|
31
|
+
...memoryPayload(),
|
|
32
|
+
memories: loadMemories("global"),
|
|
33
|
+
project_memories: loadMemories("project"),
|
|
34
|
+
});
|
|
6
35
|
return true;
|
|
7
36
|
}
|
|
8
37
|
if (req.method === "POST" && pathname === "/v1/memory") {
|