@vincemakes/kiso-code 0.39.1 → 0.40.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 +1 -1
- package/dist/attachments.d.ts +1 -1
- package/dist/attachments.js +11 -4
- package/dist/builtin.d.ts +12 -0
- package/dist/builtin.js +22 -1
- package/dist/chat.d.ts +28 -2
- package/dist/chat.js +143 -25
- package/dist/config.d.ts +11 -1
- package/dist/config.js +48 -3
- package/dist/dispatch.d.ts +10 -2
- package/dist/dispatch.js +138 -23
- package/dist/floor.d.ts +80 -0
- package/dist/floor.js +652 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +397 -44
- package/dist/mode.d.ts +12 -4
- package/dist/mode.js +32 -10
- package/dist/projects.d.ts +106 -0
- package/dist/projects.js +212 -0
- package/dist/protected-shell.d.ts +88 -0
- package/dist/protected-shell.js +406 -0
- package/dist/protected-writes.d.ts +29 -0
- package/dist/protected-writes.js +54 -0
- package/dist/readonly-shell.d.ts +58 -0
- package/dist/readonly-shell.js +590 -0
- package/dist/replay.d.ts +53 -0
- package/dist/replay.js +158 -0
- package/dist/resume-tail.js +4 -1
- package/dist/resume.js +3 -3
- package/dist/retries.d.ts +9 -0
- package/dist/retries.js +16 -0
- package/dist/session-cards.d.ts +33 -4
- package/dist/session-cards.js +44 -3
- package/dist/session-migration.d.ts +104 -0
- package/dist/session-migration.js +319 -0
- package/dist/shell-words.d.ts +134 -0
- package/dist/shell-words.js +628 -0
- package/dist/skill-invoke.d.ts +58 -0
- package/dist/skill-invoke.js +139 -0
- package/dist/state.d.ts +67 -1
- package/dist/state.js +98 -2
- package/dist/trust-ui.js +29 -5
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ kiso sessions list durable sessions
|
|
|
44
44
|
kiso help usage + configuration reference
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Sessions live under `~/.kiso/
|
|
47
|
+
Sessions live under `~/.kiso/projects/<project>/`, one folder per project; kill the process — `kill -9`
|
|
48
48
|
included — and `kiso resume` continues exactly where the log ends.
|
|
49
49
|
|
|
50
50
|
**Platforms:** macOS / Linux (Node ≥ 22). Windows is unsupported.
|
package/dist/attachments.d.ts
CHANGED
|
@@ -38,4 +38,4 @@ export declare function sniff(buf: Buffer): "image/png" | "image/jpeg" | "image/
|
|
|
38
38
|
* standing in the text. Silently dropping it would tell the model about
|
|
39
39
|
* a file it cannot see, which is worse than telling it nothing.
|
|
40
40
|
*/
|
|
41
|
-
export declare function attachImages(text: string, files?: ReadonlyMap<number, string
|
|
41
|
+
export declare function attachImages(text: string, files?: ReadonlyMap<number, string>, protectedFiles?: readonly string[]): string | ContentBlock[];
|
package/dist/attachments.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* hand its temp file to when it lands.
|
|
22
22
|
*/
|
|
23
23
|
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { isProtectedPath, protectedIdentity } from "@vincemakes/kiso-tools-node";
|
|
24
25
|
/**
|
|
25
26
|
* The size a single image may reach before it is refused.
|
|
26
27
|
*
|
|
@@ -55,7 +56,12 @@ const CANDIDATES = [
|
|
|
55
56
|
/"([^"]*\.(?:png|jpe?g|gif|webp))"/gi,
|
|
56
57
|
/(\S*\.(?:png|jpe?g|gif|webp))/gi,
|
|
57
58
|
];
|
|
58
|
-
function look(path) {
|
|
59
|
+
function look(path, id) {
|
|
60
|
+
// a protected file is never attached, image or not: a user may list a
|
|
61
|
+
// scan of a passport, and a path in a turn can come from a model's
|
|
62
|
+
// delegate task or a command's output, not only from the person
|
|
63
|
+
if (isProtectedPath(path, id))
|
|
64
|
+
return null;
|
|
59
65
|
try {
|
|
60
66
|
const st = statSync(path);
|
|
61
67
|
if (!st.isFile() || st.size > MAX_IMAGE_BYTES || st.size === 0)
|
|
@@ -83,7 +89,8 @@ function look(path) {
|
|
|
83
89
|
* standing in the text. Silently dropping it would tell the model about
|
|
84
90
|
* a file it cannot see, which is worse than telling it nothing.
|
|
85
91
|
*/
|
|
86
|
-
export function attachImages(text, files) {
|
|
92
|
+
export function attachImages(text, files, protectedFiles = []) {
|
|
93
|
+
const id = protectedIdentity(protectedFiles);
|
|
87
94
|
const found = [];
|
|
88
95
|
const claimed = [];
|
|
89
96
|
// REL-0152-D16: the `[Image #N]` capsules first. The buffer carries a
|
|
@@ -97,7 +104,7 @@ export function attachImages(text, files) {
|
|
|
97
104
|
const path = files.get(Number(m[1]));
|
|
98
105
|
if (path === undefined)
|
|
99
106
|
continue;
|
|
100
|
-
const hit = look(path);
|
|
107
|
+
const hit = look(path, id);
|
|
101
108
|
if (hit === null)
|
|
102
109
|
continue;
|
|
103
110
|
claimed.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -112,7 +119,7 @@ export function attachImages(text, files) {
|
|
|
112
119
|
const end = start + m[0].length;
|
|
113
120
|
if (claimed.some((c) => start < c.end && end > c.start))
|
|
114
121
|
continue;
|
|
115
|
-
const hit = look(path);
|
|
122
|
+
const hit = look(path, id);
|
|
116
123
|
if (hit === null)
|
|
117
124
|
continue;
|
|
118
125
|
claimed.push({ start, end });
|
package/dist/builtin.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { type AskUI } from "@vincemakes/kiso-ask-ext";
|
|
2
2
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* 0.40.0 (the owner's dogfood): dontAsk never asks, so its tool table never
|
|
5
|
+
* offers ask_user — a model once put four questions to a dontAsk session
|
|
6
|
+
* that could only decline them. A LIVE gate, not a load-time one: the
|
|
7
|
+
* registry reads an extension's tools on every request (registerLive) and
|
|
8
|
+
* the run recomposes the tool table, snippet and guidelines included, so
|
|
9
|
+
* entering dontAsk takes ask_user away from the next run and leaving it
|
|
10
|
+
* brings ask_user back. In dontAsk the table is byte-identical to the pipe
|
|
11
|
+
* path's; each switch in or out costs one prompt-cache break. The decline
|
|
12
|
+
* path (trust-ui) stays for a turn already in flight when the mode changes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function offInDontAsk(ext: KisoExtension): KisoExtension;
|
|
3
15
|
export declare function builtInLayer(user: readonly KisoExtension[], project: readonly KisoExtension[],
|
|
4
16
|
/** KC3.5: built-in #4 — the ask extension, loaded ONLY when a panel
|
|
5
17
|
* bridge exists (an interactive TTY). No bridge, no fourth built-in:
|
package/dist/builtin.js
CHANGED
|
@@ -25,6 +25,27 @@ import createMcp from "@vincemakes/kiso-mcp-ext";
|
|
|
25
25
|
import createSkills from "@vincemakes/kiso-skills-ext";
|
|
26
26
|
import createSubagent from "@vincemakes/kiso-subagent-ext";
|
|
27
27
|
import createAsk, {} from "@vincemakes/kiso-ask-ext";
|
|
28
|
+
import { getMode } from "./mode.js";
|
|
29
|
+
/**
|
|
30
|
+
* 0.40.0 (the owner's dogfood): dontAsk never asks, so its tool table never
|
|
31
|
+
* offers ask_user — a model once put four questions to a dontAsk session
|
|
32
|
+
* that could only decline them. A LIVE gate, not a load-time one: the
|
|
33
|
+
* registry reads an extension's tools on every request (registerLive) and
|
|
34
|
+
* the run recomposes the tool table, snippet and guidelines included, so
|
|
35
|
+
* entering dontAsk takes ask_user away from the next run and leaving it
|
|
36
|
+
* brings ask_user back. In dontAsk the table is byte-identical to the pipe
|
|
37
|
+
* path's; each switch in or out costs one prompt-cache break. The decline
|
|
38
|
+
* path (trust-ui) stays for a turn already in flight when the mode changes.
|
|
39
|
+
*/
|
|
40
|
+
export function offInDontAsk(ext) {
|
|
41
|
+
const tools = ext.tools ?? [];
|
|
42
|
+
return {
|
|
43
|
+
...ext,
|
|
44
|
+
get tools() {
|
|
45
|
+
return getMode() === "dontAsk" ? [] : tools;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
28
49
|
export async function builtInLayer(user, project,
|
|
29
50
|
/** KC3.5: built-in #4 — the ask extension, loaded ONLY when a panel
|
|
30
51
|
* bridge exists (an interactive TTY). No bridge, no fourth built-in:
|
|
@@ -36,7 +57,7 @@ ask,
|
|
|
36
57
|
* constructed, so the names have to arrive here — after construction is
|
|
37
58
|
* too late. Only the config knows them; nothing else can. */
|
|
38
59
|
secretEnvNames = []) {
|
|
39
|
-
const all = await Promise.all([createMcp({ secretEnvNames }), createSkills(), createSubagent(), ...(ask === undefined ? [] : [createAsk(ask)])]);
|
|
60
|
+
const all = await Promise.all([createMcp({ secretEnvNames }), createSkills(), createSubagent(), ...(ask === undefined ? [] : [createAsk(ask).then(offInDontAsk)])]);
|
|
40
61
|
const shadowed = all.filter((b) => user.some((u) => u.name === b.name));
|
|
41
62
|
for (const s of shadowed) {
|
|
42
63
|
console.error(`[extensions] user extension "${s.name}" shadows the built-in — the built-in is not loaded`);
|
package/dist/chat.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* approval-moment mini-diff, the status spinner, and the context
|
|
5
5
|
* estimates. All bodies moved verbatim from index.ts.
|
|
6
6
|
*/
|
|
7
|
+
import type { SessionRoute } from "./projects.js";
|
|
7
8
|
import { type RunUsage } from "@vincemakes/kiso-tui";
|
|
8
9
|
import { type SaferFailure, type SaferOption } from "@vincemakes/kiso-tui";
|
|
9
10
|
import type { AgentSession, Run } from "@vincemakes/kiso-runtime";
|
|
@@ -79,8 +80,10 @@ export declare function microcompactThresholdFor(of?: {
|
|
|
79
80
|
readonly baseUrl?: string;
|
|
80
81
|
}): number;
|
|
81
82
|
/**
|
|
82
|
-
* B area: approximate context ratio
|
|
83
|
-
*
|
|
83
|
+
* B area: approximate context ratio vs the model window. Marked ~
|
|
84
|
+
* everywhere it is shown. 0.40.0: the context as the last bill measured it
|
|
85
|
+
* (session.contextUsed — the estimate only when no bill describes it);
|
|
86
|
+
* chars/4 alone read the owner's 730k Chinese-heavy context as ~470k.
|
|
84
87
|
*/
|
|
85
88
|
export declare function estimateCtxRatio(session: AgentSession): number;
|
|
86
89
|
/** A1a: the ratio the STATUS LINE and the ctx displays show — the request
|
|
@@ -98,6 +101,27 @@ export declare function statusModelLabel(session: {
|
|
|
98
101
|
};
|
|
99
102
|
}): string;
|
|
100
103
|
export declare function displayCtxRatio(session: AgentSession): number;
|
|
104
|
+
/** 0.40.0 item 9: how long a prompt cache is assumed to live. PROVISIONAL:
|
|
105
|
+
* DeepSeek does not publish its TTL (the owner's cache was gone after 27
|
|
106
|
+
* minutes); Anthropic's default is 5 minutes. */
|
|
107
|
+
export declare const COLD_AFTER_MS: number;
|
|
108
|
+
/** 0.40.0 (the owner): the recap's cold-cache wording — only when the turn
|
|
109
|
+
* began at least COLD_AFTER_MS after the last bill AND the cache really
|
|
110
|
+
* missed (a surfaced miss, or under half the prompt from cache). */
|
|
111
|
+
export declare function coldAfter(idleMs: number | undefined, missed: number | null, usage: RunUsage): {
|
|
112
|
+
coldAfterMinutes?: number;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* 0.40.0 item 9 — the cold resume. When the last BILL put the context over
|
|
116
|
+
* the microcompact threshold and that bill is older than COLD_AFTER_MS, the
|
|
117
|
+
* next request re-sends the whole prefix uncached. Compacting first turns
|
|
118
|
+
* that one expensive request into a summary call. Anchored only: a session
|
|
119
|
+
* no bill describes has no known size, and no age to call cold.
|
|
120
|
+
*/
|
|
121
|
+
export declare function coldResumeOffer(session: AgentSession, now?: number): {
|
|
122
|
+
tokens: number;
|
|
123
|
+
minutes: number;
|
|
124
|
+
} | null;
|
|
101
125
|
/** A1a: the number the auto-compact decision reads — the pre-A1a estimate,
|
|
102
126
|
* unchanged this round, named so a gate can pin that it did not move. */
|
|
103
127
|
export declare function autoCompactRatio(session: AgentSession): number;
|
|
@@ -298,6 +322,8 @@ export type ChatEnd = {
|
|
|
298
322
|
* ids, and (when a dock is up) the existing picker. */
|
|
299
323
|
export interface ChatNav {
|
|
300
324
|
readonly sessions: () => readonly string[];
|
|
325
|
+
/** 0.40.0: an id outside this project's folder — refused, or opened where it is */
|
|
326
|
+
readonly route?: (id: string) => SessionRoute | null;
|
|
301
327
|
readonly pick?: () => Promise<string | null>;
|
|
302
328
|
}
|
|
303
329
|
export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav): Promise<ChatEnd>;
|
package/dist/chat.js
CHANGED
|
@@ -6,19 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, verifyOfferView, STATUS_GLYPHS, } from "@vincemakes/kiso-tui";
|
|
9
|
-
import { askView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
|
-
import { canonicalTargetPath, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
9
|
+
import { askView, coldResumeLine, coldResumeView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
|
+
import { canonicalTargetPath, isProtectedPath, protectedIdentity, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
11
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
12
12
|
import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
13
13
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
14
14
|
import { dispatch, abortBangCommand } from "./dispatch.js";
|
|
15
15
|
import { paintWindowTitle } from "./window-title.js";
|
|
16
|
-
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
16
|
+
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock, retryOnRow, retryShown, setRetryShown, floorOn, protectedFiles } from "./state.js";
|
|
17
17
|
import { attachImages } from "./attachments.js";
|
|
18
18
|
import { lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
19
19
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
20
20
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
21
|
-
import {
|
|
21
|
+
import { OFFERED_MODES, getMode, setMode } from "./mode.js";
|
|
22
22
|
/** B area: default context window for the ~ctx estimate (config overridable). */
|
|
23
23
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
24
24
|
/** The in-process fake provider's id, and the window we declare for it.
|
|
@@ -134,13 +134,13 @@ export function microcompactThresholdFor(of) {
|
|
|
134
134
|
return contextWindowTokens(of) / 2;
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
|
-
* B area: approximate context ratio
|
|
138
|
-
*
|
|
137
|
+
* B area: approximate context ratio vs the model window. Marked ~
|
|
138
|
+
* everywhere it is shown. 0.40.0: the context as the last bill measured it
|
|
139
|
+
* (session.contextUsed — the estimate only when no bill describes it);
|
|
140
|
+
* chars/4 alone read the owner's 730k Chinese-heavy context as ~470k.
|
|
139
141
|
*/
|
|
140
142
|
export function estimateCtxRatio(session) {
|
|
141
|
-
|
|
142
|
-
const chars = JSON.stringify(projected).length;
|
|
143
|
-
return chars / 4 / contextWindowTokens();
|
|
143
|
+
return session.contextUsed() / contextWindowTokens();
|
|
144
144
|
}
|
|
145
145
|
/** A1a: the ratio the STATUS LINE and the ctx displays show — the request
|
|
146
146
|
* budget counting the parts (system prompt, tool table, messages with
|
|
@@ -164,8 +164,42 @@ export function displayCtxRatio(session) {
|
|
|
164
164
|
const window = knownContextWindow();
|
|
165
165
|
if (window === null)
|
|
166
166
|
return Number.NaN;
|
|
167
|
+
// 0.40.0: the last bill is the truth when one describes the context;
|
|
168
|
+
// the parts estimate is for a session no bill describes yet.
|
|
169
|
+
const anchored = session.contextAnchor();
|
|
170
|
+
if (anchored !== undefined)
|
|
171
|
+
return anchored / window;
|
|
167
172
|
return requestBudget(session.requestParts(), window).ratio;
|
|
168
173
|
}
|
|
174
|
+
/** 0.40.0 item 9: how long a prompt cache is assumed to live. PROVISIONAL:
|
|
175
|
+
* DeepSeek does not publish its TTL (the owner's cache was gone after 27
|
|
176
|
+
* minutes); Anthropic's default is 5 minutes. */
|
|
177
|
+
export const COLD_AFTER_MS = 5 * 60_000;
|
|
178
|
+
/** 0.40.0 (the owner): the recap's cold-cache wording — only when the turn
|
|
179
|
+
* began at least COLD_AFTER_MS after the last bill AND the cache really
|
|
180
|
+
* missed (a surfaced miss, or under half the prompt from cache). */
|
|
181
|
+
export function coldAfter(idleMs, missed, usage) {
|
|
182
|
+
if (idleMs === undefined || idleMs < COLD_AFTER_MS)
|
|
183
|
+
return {};
|
|
184
|
+
const hit = cacheHitPct(usage);
|
|
185
|
+
return missed !== null || (hit !== null && hit < 50) ? { coldAfterMinutes: Math.round(idleMs / 60_000) } : {};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 0.40.0 item 9 — the cold resume. When the last BILL put the context over
|
|
189
|
+
* the microcompact threshold and that bill is older than COLD_AFTER_MS, the
|
|
190
|
+
* next request re-sends the whole prefix uncached. Compacting first turns
|
|
191
|
+
* that one expensive request into a summary call. Anchored only: a session
|
|
192
|
+
* no bill describes has no known size, and no age to call cold.
|
|
193
|
+
*/
|
|
194
|
+
export function coldResumeOffer(session, now = Date.now()) {
|
|
195
|
+
const tokens = session.contextAnchor();
|
|
196
|
+
const at = session.lastUsageAt;
|
|
197
|
+
if (tokens === undefined || at === undefined)
|
|
198
|
+
return null;
|
|
199
|
+
if (tokens <= microcompactThresholdFor() || now - at < COLD_AFTER_MS)
|
|
200
|
+
return null;
|
|
201
|
+
return { tokens, minutes: Math.floor((now - at) / 60_000) };
|
|
202
|
+
}
|
|
169
203
|
/** A1a: the number the auto-compact decision reads — the pre-A1a estimate,
|
|
170
204
|
* unchanged this round, named so a gate can pin that it did not move. */
|
|
171
205
|
export function autoCompactRatio(session) {
|
|
@@ -332,6 +366,11 @@ export function turnUsageLedger() {
|
|
|
332
366
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
333
367
|
* first event. */
|
|
334
368
|
export function startStatusSpinner(onTick) {
|
|
369
|
+
// ADR-0005 Amendment 2: a retry belongs to the run that announced it.
|
|
370
|
+
// Cleared where every run starts and stops — the chat's and the
|
|
371
|
+
// recovery flow's alike — so a stale countdown can never survive into
|
|
372
|
+
// the idle row or the next turn.
|
|
373
|
+
setRetryShown(null);
|
|
335
374
|
if (!dock.active)
|
|
336
375
|
return () => { };
|
|
337
376
|
// v3 §03/§05: the working glyph family ▖▘▝▗, 200ms rotation — the
|
|
@@ -340,7 +379,10 @@ export function startStatusSpinner(onTick) {
|
|
|
340
379
|
let i = 0;
|
|
341
380
|
const timer = setInterval(() => onTick(STATUS_GLYPHS[i++ % STATUS_GLYPHS.length]), 200);
|
|
342
381
|
timer.unref();
|
|
343
|
-
return () =>
|
|
382
|
+
return () => {
|
|
383
|
+
clearInterval(timer);
|
|
384
|
+
setRetryShown(null);
|
|
385
|
+
};
|
|
344
386
|
}
|
|
345
387
|
/**
|
|
346
388
|
* TUI2-R1 (C) — the shell tailer: the READER half of the progress
|
|
@@ -391,6 +433,10 @@ function approvalDiff(name, input) {
|
|
|
391
433
|
const path = typeof input.path === "string" ? input.path : "";
|
|
392
434
|
if (path === "")
|
|
393
435
|
return null;
|
|
436
|
+
// the tool refuses a protected file whatever the answer; the panel
|
|
437
|
+
// must not print the store on the way there
|
|
438
|
+
if (isProtectedPath(path, protectedIdentity(protectedFiles())))
|
|
439
|
+
return null;
|
|
394
440
|
let oldContent = null;
|
|
395
441
|
try {
|
|
396
442
|
oldContent = readFileSync(path, "utf8");
|
|
@@ -745,6 +791,9 @@ submitTurn) {
|
|
|
745
791
|
// seconds, usage, ctx left. The turn's WORK is the fold line's, said
|
|
746
792
|
// once, where the work happened.
|
|
747
793
|
const turnStart = Date.now();
|
|
794
|
+
// 0.40.0 (the owner): how long the session had been idle when this turn
|
|
795
|
+
// began — the recap names a cold cache instead of a bare fresh figure.
|
|
796
|
+
const idleMs = session.lastUsageAt === undefined ? undefined : turnStart - session.lastUsageAt;
|
|
748
797
|
// W14: the thinking event carries NO timestamp — the CLI wall-clocks
|
|
749
798
|
// the thinking window: it opens at the first thinking event and closes
|
|
750
799
|
// at the first non-thinking event (the fold needs the seconds).
|
|
@@ -763,11 +812,21 @@ submitTurn) {
|
|
|
763
812
|
try {
|
|
764
813
|
for await (const ev of run) {
|
|
765
814
|
last = ev;
|
|
815
|
+
// ADR-0005 Amendment 2: an event from the run means the retried
|
|
816
|
+
// attempt got through — the row stops saying it is waiting.
|
|
817
|
+
if (retryShown !== null)
|
|
818
|
+
setRetryShown(null);
|
|
766
819
|
// LT2B-F1: a turn count is not evidence of a loop. Keep consuming
|
|
767
820
|
// healthy runs without a periodic presence check; explicit cancellation,
|
|
768
821
|
// the stream watchdog and the repeated-failure breaker remain active.
|
|
769
822
|
// W22-R1: the ledger sees every event; a stop is what ends a call.
|
|
770
823
|
ledger.observe(ev);
|
|
824
|
+
// ADR-0055 Amendment 1 (A1b): a compaction inside the run says so, once.
|
|
825
|
+
if (ev.type === "summarized" || ev.type === "microcompacted") {
|
|
826
|
+
const r = displayCtxRatio(session);
|
|
827
|
+
const ctx = Number.isFinite(r) ? ` · ctx now ~${Math.round(r * 100)}% used` : "";
|
|
828
|
+
body.notice(ev.type === "summarized" ? `✦ compacted mid-run — the conversation before this point is a summary now${ctx}` : `✦ pruned old tool output mid-run${ctx}`);
|
|
829
|
+
}
|
|
771
830
|
if (ev.type !== "thinking") {
|
|
772
831
|
if (thinkingSince !== null) {
|
|
773
832
|
thoughtSeconds += (Date.now() - thinkingSince) / 1000;
|
|
@@ -800,6 +859,12 @@ submitTurn) {
|
|
|
800
859
|
body.notice(` ${typeof ev.content === "string" ? ev.content : ""}`);
|
|
801
860
|
break;
|
|
802
861
|
}
|
|
862
|
+
// 0.40.0: a skill turn's chip is the line the person TYPED — the
|
|
863
|
+
// SKILL.md body is what the model read, not what they said.
|
|
864
|
+
if (ev.via !== undefined) {
|
|
865
|
+
body.userLine(ev.via.line);
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
803
868
|
// DC-60: a turn that carries an image is a content ARRAY — its words
|
|
804
869
|
// still echo, the image as a mark; an empty chip was the bug.
|
|
805
870
|
body.userLine(echoText(ev.content));
|
|
@@ -912,8 +977,17 @@ submitTurn) {
|
|
|
912
977
|
// - Yes+amend → approve(true), the words ride the NEXT turn;
|
|
913
978
|
// - esc → cancel, the conservative denial.
|
|
914
979
|
const name = ev.name;
|
|
915
|
-
body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
|
|
916
980
|
const decisionId = ev.decisionId;
|
|
981
|
+
// Launch-weekend plan §2 — dontAsk: the chain's final ASK is
|
|
982
|
+
// denied HERE, at the one place kiso asks, so every allow the
|
|
983
|
+
// chain gave still stands (a tier deny would outrank them). The
|
|
984
|
+
// reason is the tool result: the model sees why and goes on.
|
|
985
|
+
if (getMode() === "dontAsk") {
|
|
986
|
+
body.notice(`[dontAsk] ${escapeTerminal(name)} would ask — denied`);
|
|
987
|
+
await session.approve(decisionId, false, `dontAsk: ${name} needs a human's approval, and this session never asks — denied`);
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
|
|
917
991
|
// TUI2-R3v2 ③: the on-demand alternatives provider. It is built
|
|
918
992
|
// per approval and captured by the panel; it fires ONLY if the
|
|
919
993
|
// human presses option 3, which is the whole zero-ambient-rent
|
|
@@ -1054,6 +1128,10 @@ submitTurn) {
|
|
|
1054
1128
|
// R-C item 4: only an above-floor miss is surfaced —
|
|
1055
1129
|
// the recap gains "· miss N" on the cache segment.
|
|
1056
1130
|
...(missed !== null ? { missed } : {}),
|
|
1131
|
+
// Cold: idle past the cache's assumed life (the #77
|
|
1132
|
+
// constant, provisional) AND the turn did re-send a
|
|
1133
|
+
// prefix uncached — the time alone is not evidence.
|
|
1134
|
+
...(coldAfter(idleMs, missed, turnUsage() ?? UNKNOWN_USAGE)),
|
|
1057
1135
|
ctxLeftPct: Number.isFinite(ratio) ? (1 - ratio) * 100 : null,
|
|
1058
1136
|
// W19: under plan the recap becomes the way-forward row
|
|
1059
1137
|
// (the /mode hints are the mode's exits).
|
|
@@ -1116,7 +1194,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1116
1194
|
console.log("\n[exit requested]");
|
|
1117
1195
|
input.close();
|
|
1118
1196
|
};
|
|
1119
|
-
const turn = (text, seedSource) => new Promise((resolve, reject) => {
|
|
1197
|
+
const turn = (text, seedSource, via) => new Promise((resolve, reject) => {
|
|
1120
1198
|
queued = Math.max(0, queued - 1); // a queued turn starts
|
|
1121
1199
|
// REL-0152-D11: a turn that names an image file carries it. The
|
|
1122
1200
|
// scan returns the STRING unchanged when it finds nothing, so a
|
|
@@ -1126,8 +1204,12 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1126
1204
|
// REL-0152-D16: the capsules' files come from the editor, which
|
|
1127
1205
|
// is the only thing that knows which number stands for which
|
|
1128
1206
|
// screenshot.
|
|
1129
|
-
|
|
1130
|
-
|
|
1207
|
+
//
|
|
1208
|
+
// 0.40.0: a skill turn is not scanned either — its text is a
|
|
1209
|
+
// SKILL.md body, and a body that mentions `diagram.png` must not
|
|
1210
|
+
// attach a file from the workspace the person never pointed at.
|
|
1211
|
+
const content = seedSource !== undefined || via !== undefined ? text : attachImages(text, input.attachments?.(), protectedFiles());
|
|
1212
|
+
const run = seedSource !== undefined ? session.run(content, { source: seedSource }) : via !== undefined ? session.run(content, { via }) : session.run(content);
|
|
1131
1213
|
currentRun = run;
|
|
1132
1214
|
turnNo += 1;
|
|
1133
1215
|
const myTurn = turnNo;
|
|
@@ -1278,11 +1360,15 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1278
1360
|
// BEHIND the correction, in their original order. Ephemeral
|
|
1279
1361
|
// reordering of ephemeral state; the durable log still just records
|
|
1280
1362
|
// what ran, in the order it ran.
|
|
1281
|
-
|
|
1363
|
+
// A slot re-enters with its CONTENT and its `via`, never its display
|
|
1364
|
+
// line: a queued skill re-submitted as `/review x` would reach the
|
|
1365
|
+
// model as the literal text of the command.
|
|
1366
|
+
const jumped = pendingTurns.map((s) => ({ content: s.content, via: s.via }));
|
|
1282
1367
|
for (let i = jumped.length; i > 0; i -= 1)
|
|
1283
1368
|
popQueue();
|
|
1284
|
-
|
|
1285
|
-
|
|
1369
|
+
submitTurn(line);
|
|
1370
|
+
for (const j of jumped)
|
|
1371
|
+
submitTurn(j.content, j.via);
|
|
1286
1372
|
});
|
|
1287
1373
|
// round 5 (P1-11): the PERSISTENT line listener is installed BEFORE the
|
|
1288
1374
|
// startup recovery — a cancelled question's re-emitted "line" needs a
|
|
@@ -1304,6 +1390,9 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1304
1390
|
// read (the dock renders the lines, the editor pops the last one).
|
|
1305
1391
|
// A slot leaves the queue when its turn STARTS or when the user
|
|
1306
1392
|
// pops it (cancelled — the chain segment skips it).
|
|
1393
|
+
// 0.40.0: `line` is what the chip shows and what a pop hands back to
|
|
1394
|
+
// the editor — for a skill, the line the person TYPED; `content` is
|
|
1395
|
+
// what the turn submits (the skill's body and args).
|
|
1307
1396
|
const pendingTurns = [];
|
|
1308
1397
|
// TV-1B — the offer memory: session-local BY DESIGN (a dead process's
|
|
1309
1398
|
// "not now" should not silence a live one; resume re-offers once,
|
|
@@ -1344,8 +1433,12 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1344
1433
|
// KC2 §5: the STATE (the glyph, the run's start, the usage, the dock)
|
|
1345
1434
|
// stays here; the ROW's text is the tui's status formatter.
|
|
1346
1435
|
const paintRunning = () => {
|
|
1436
|
+
// ADR-0005 Amendment 2: the running row gets the idle row's budget
|
|
1437
|
+
// (DF-0330-F1). A pending retry makes it thirty columns longer, and
|
|
1438
|
+
// composed blind it would be cut from the END — the context figure,
|
|
1439
|
+
// a fact, going before the gesture hints.
|
|
1347
1440
|
if (dock.active)
|
|
1348
|
-
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec));
|
|
1441
|
+
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec, rowWidth(), retryOnRow()));
|
|
1349
1442
|
};
|
|
1350
1443
|
// W19: under plan the idle row makes the posture unmistakable — the W4
|
|
1351
1444
|
// parentheses idiom names the read-only constraint. The tier is the
|
|
@@ -1364,7 +1457,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1364
1457
|
// DF-0330-F1: the row's budget. Without it the row is composed
|
|
1365
1458
|
// blind and invariant ① cuts whatever sits last — which is how
|
|
1366
1459
|
// a measured rate went missing at 100 columns.
|
|
1367
|
-
rowWidth()));
|
|
1460
|
+
rowWidth(), !floorOn));
|
|
1368
1461
|
};
|
|
1369
1462
|
// TUI2-R2 ⑥ — the BOOT status line. The row is the product's one
|
|
1370
1463
|
// persistent claim about itself (the tier, how to change it, the model,
|
|
@@ -1408,8 +1501,8 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1408
1501
|
return;
|
|
1409
1502
|
spentUsd = (spentUsd ?? 0) + usd;
|
|
1410
1503
|
};
|
|
1411
|
-
const submitTurn = (line) => {
|
|
1412
|
-
const slot = { line, cancelled: false };
|
|
1504
|
+
const submitTurn = (line, via) => {
|
|
1505
|
+
const slot = { line: via?.line ?? line, content: line, ...(via !== undefined ? { via } : {}), cancelled: false };
|
|
1413
1506
|
pendingTurns.push(slot);
|
|
1414
1507
|
queued += 1;
|
|
1415
1508
|
chainRef.current = chainRef.current.then(async () => {
|
|
@@ -1433,7 +1526,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1433
1526
|
if (session.uncertainExecutions().length > 0)
|
|
1434
1527
|
await resolveUncertains(session, input, () => cancelled);
|
|
1435
1528
|
if (session.uncertainExecutions().length === 0)
|
|
1436
|
-
return turn(line);
|
|
1529
|
+
return turn(line, undefined, via);
|
|
1437
1530
|
// The human declined (round 10: a cancelled ask records NOTHING —
|
|
1438
1531
|
// the execution stays uncertain and durable), so the turn does not
|
|
1439
1532
|
// start. It is never swallowed in silence: the held text is
|
|
@@ -1481,6 +1574,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1481
1574
|
resolveEnd();
|
|
1482
1575
|
},
|
|
1483
1576
|
sessions: () => nav?.sessions() ?? [],
|
|
1577
|
+
...(nav?.route !== undefined ? { route: nav.route } : {}),
|
|
1484
1578
|
...(nav?.pick !== undefined ? { pickSession: nav.pick } : {}),
|
|
1485
1579
|
};
|
|
1486
1580
|
// the ergonomics batch C8: the auto-compact check — the /compact FULL path via the
|
|
@@ -1525,10 +1619,11 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1525
1619
|
// they are the same sentinel: one implementation, one behaviour.
|
|
1526
1620
|
input.onCopy(() => dispatch("\x18copy", dispatchCtx));
|
|
1527
1621
|
// R3a — Shift+Tab: the approval-tier cycle (the /mode ring, in the
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1622
|
+
// OFFERED order — a session started in `manual` steps to the first).
|
|
1623
|
+
// The switch is the SAME live-extension flip /mode performs; the status
|
|
1624
|
+
// row repaints at once with a one-line notice.
|
|
1530
1625
|
input.onModeCycle?.(() => {
|
|
1531
|
-
const next =
|
|
1626
|
+
const next = OFFERED_MODES[(OFFERED_MODES.indexOf(getMode()) + 1) % OFFERED_MODES.length];
|
|
1532
1627
|
setMode(next);
|
|
1533
1628
|
paintIdle();
|
|
1534
1629
|
body.notice(`mode → ${next} (shift+tab cycles)`);
|
|
@@ -1539,6 +1634,29 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1539
1634
|
// round 8: the startup resume is bound to currentRun — Ctrl+C during it
|
|
1540
1635
|
// aborts the recovery, exactly like the interactive turns.
|
|
1541
1636
|
await resolveUncertains(session, input, () => cancelled);
|
|
1637
|
+
// 0.40.0 item 9: a big session whose cache has gone cold compacts BEFORE
|
|
1638
|
+
// its first request, on the person's word — or without asking in
|
|
1639
|
+
// dontAsk, where compacting is not an approval. A piped session is left
|
|
1640
|
+
// to the auto policy, which fires above the hard tier on its own. Lines
|
|
1641
|
+
// typed meanwhile queue behind the compaction.
|
|
1642
|
+
//
|
|
1643
|
+
// The owner's dogfood: asked BEFORE the recovery run. A session whose
|
|
1644
|
+
// last run was cut resumes that run at once, and the resume's first
|
|
1645
|
+
// request is the whole cold prefix — asked afterwards, the question is
|
|
1646
|
+
// about a bill already paid (and the in-run tiers have usually shrunk the
|
|
1647
|
+
// context by then, so it is never asked at all). The compaction settles
|
|
1648
|
+
// before the run resumes: the run continues on the compacted projection.
|
|
1649
|
+
const cold = !cancelled && process.stdin.isTTY ? coldResumeOffer(session) : null;
|
|
1650
|
+
if (cold !== null) {
|
|
1651
|
+
if (getMode() === "dontAsk") {
|
|
1652
|
+
body.notice(`[dontAsk] ${coldResumeLine(cold.tokens, cold.minutes)} — compacting first`);
|
|
1653
|
+
dispatch("/compact", dispatchCtx);
|
|
1654
|
+
}
|
|
1655
|
+
else if ((await askPanel(input, coldResumeView(cold.tokens, cold.minutes))).action === "allow") {
|
|
1656
|
+
dispatch("/compact", dispatchCtx);
|
|
1657
|
+
}
|
|
1658
|
+
await chainRef.current;
|
|
1659
|
+
}
|
|
1542
1660
|
if (!cancelled) {
|
|
1543
1661
|
const recoveryRun = session.resume();
|
|
1544
1662
|
currentRun = recoveryRun;
|
package/dist/config.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* "anthropic" | "openai-responses")
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
15
|
-
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
|
+
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"|"dontAsk"
|
|
16
16
|
* contextWindow?: number — tokens
|
|
17
17
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
18
18
|
* projectTrust?: "ask" | "never" — no "always" (the ruling)
|
|
@@ -82,6 +82,16 @@ export interface KisoConfig {
|
|
|
82
82
|
* project file is a LOUD error rather than a silent win. `KISO_THEME`
|
|
83
83
|
* still outranks it — the environment is the more local answer. */
|
|
84
84
|
readonly theme?: "dark" | "light";
|
|
85
|
+
/** 0.40.0: the catastrophe floor (floor.ts). Default on. USER-LEVEL
|
|
86
|
+
* ONLY, and louder than theme about it: a repository that could lower
|
|
87
|
+
* the floor would be the one thing the floor exists to stop. */
|
|
88
|
+
readonly floor?: "catastrophe" | "off";
|
|
89
|
+
/** kiso never serves its own credential store to a model, and these
|
|
90
|
+
* files join it: no tool reads, writes or searches them, and no shell
|
|
91
|
+
* line may name them (protected-shell.ts). Absolute, or `~/…`. USER-
|
|
92
|
+
* LEVEL ONLY, as loud as floor: a project must never be able to change
|
|
93
|
+
* what kiso guards. */
|
|
94
|
+
readonly protectedPaths?: readonly string[];
|
|
85
95
|
/** DT-1a: named acceptance checks a delegated task may reference —
|
|
86
96
|
* user-authored (or trust-gated project) commands, run by the PARENT
|
|
87
97
|
* in the child's worktree. A model never supplies a command; it names
|
package/dist/config.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* "anthropic" | "openai-responses")
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
15
|
-
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
|
+
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"|"dontAsk"
|
|
16
16
|
* contextWindow?: number — tokens
|
|
17
17
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
18
18
|
* projectTrust?: "ask" | "never" — no "always" (the ruling)
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
* with an invalid value is likewise loud; unknown top-level keys are
|
|
28
28
|
* ignored (forward compatibility).
|
|
29
29
|
*/
|
|
30
|
-
import { readFileSync } from "node:fs";
|
|
30
|
+
import { readFileSync, statSync } from "node:fs";
|
|
31
|
+
import { homedir } from "node:os";
|
|
31
32
|
import { join } from "node:path";
|
|
32
33
|
import { kisoHome } from "./state.js";
|
|
33
34
|
import { AuthError, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
34
35
|
import { BUILTIN_MANIFESTS } from "@vincemakes/kiso-runtime/internal";
|
|
35
36
|
const KINDS = ["openai-compat", "anthropic", "openai-responses"];
|
|
36
|
-
const MODES_LIST = ["manual", "default", "accept-edits", "plan", "bypass"];
|
|
37
|
+
const MODES_LIST = ["manual", "default", "accept-edits", "plan", "bypass", "dontAsk"];
|
|
37
38
|
export class ConfigError extends Error {
|
|
38
39
|
}
|
|
39
40
|
/** Parse + validate one config file. Broken JSON / invalid known values →
|
|
@@ -66,6 +67,36 @@ export function parseConfig(text, source) {
|
|
|
66
67
|
fail("theme", "belongs in the USER config — a terminal is a property of the person at it, not of the project");
|
|
67
68
|
out.theme = obj.theme;
|
|
68
69
|
}
|
|
70
|
+
if (obj.floor !== undefined) {
|
|
71
|
+
if (obj.floor !== "catastrophe" && obj.floor !== "off")
|
|
72
|
+
fail("floor", 'expected "catastrophe" or "off"');
|
|
73
|
+
if (source.startsWith("<cwd>"))
|
|
74
|
+
fail("floor", "belongs in the USER config — a project must never be able to lower the floor");
|
|
75
|
+
out.floor = obj.floor;
|
|
76
|
+
}
|
|
77
|
+
if (obj.protectedPaths !== undefined) {
|
|
78
|
+
if (source.startsWith("<cwd>"))
|
|
79
|
+
fail("protectedPaths", "belongs in the USER config — a project must never be able to change what kiso guards");
|
|
80
|
+
if (!Array.isArray(obj.protectedPaths))
|
|
81
|
+
fail("protectedPaths", "expected an array of paths");
|
|
82
|
+
for (const [i, p] of obj.protectedPaths.entries()) {
|
|
83
|
+
// a relative path would be read against whatever directory kiso
|
|
84
|
+
// runs in — a different file in every project
|
|
85
|
+
if (typeof p !== "string" || !(p.startsWith("/") || p.startsWith("~/")))
|
|
86
|
+
fail(`protectedPaths[${i}]`, "expected an absolute path or one starting with ~/");
|
|
87
|
+
// kiso protects FILES. A directory listed here would protect
|
|
88
|
+
// nothing under it — a security setting that silently does
|
|
89
|
+
// nothing — so it is refused where the person can see it.
|
|
90
|
+
// (Read at every agent build: a directory created later is
|
|
91
|
+
// caught at the next start or /reload.)
|
|
92
|
+
const path = p;
|
|
93
|
+
const full = path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
94
|
+
if (path.endsWith("/") || statSync(full, { throwIfNoEntry: false })?.isDirectory() === true) {
|
|
95
|
+
fail(`protectedPaths[${i}]`, `${path} is a directory — kiso protects files, so list the files in it (for ~/.aws: "~/.aws/credentials", "~/.aws/config")`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
out.protectedPaths = obj.protectedPaths;
|
|
99
|
+
}
|
|
69
100
|
if (obj.checks !== undefined) {
|
|
70
101
|
if (obj.checks === null || typeof obj.checks !== "object" || Array.isArray(obj.checks))
|
|
71
102
|
fail("checks", "expected an object of name → command");
|
|
@@ -102,6 +133,19 @@ export function parseConfig(text, source) {
|
|
|
102
133
|
fail(`models.${name}.promptCaching`, "expected a boolean");
|
|
103
134
|
if (p.streamIdleMs !== undefined && (typeof p.streamIdleMs !== "number" || !Number.isFinite(p.streamIdleMs) || p.streamIdleMs < 0))
|
|
104
135
|
fail(`models.${name}.streamIdleMs`, "expected a non-negative number of milliseconds (0 disables the stream watchdog)");
|
|
136
|
+
// 0.39.2: `contextWindow` was DECLARED on this type, VALIDATED at
|
|
137
|
+
// the top level, and CONSUMED by resolveContextWindow — which
|
|
138
|
+
// prefers it over the global figure, with a comment saying why —
|
|
139
|
+
// and it was never copied here. So it never reached anything: 70
|
|
140
|
+
// of 70 profiles that declared one lost it in this object
|
|
141
|
+
// literal. The cost was not the `ctx ?` on the status row but the
|
|
142
|
+
// compaction threshold, which fell through to the 200k default
|
|
143
|
+
// for every model the registry does not carry — a 1M-window model
|
|
144
|
+
// compacting as though it had a fifth of its window. The
|
|
145
|
+
// documented workaround for exactly that ("set contextWindow in
|
|
146
|
+
// your profile") had never done anything.
|
|
147
|
+
if (p.contextWindow !== undefined && (typeof p.contextWindow !== "number" || !Number.isFinite(p.contextWindow) || p.contextWindow <= 0))
|
|
148
|
+
fail(`models.${name}.contextWindow`, "expected a positive token count");
|
|
105
149
|
models[name] = {
|
|
106
150
|
kind: p.kind,
|
|
107
151
|
model: p.model,
|
|
@@ -109,6 +153,7 @@ export function parseConfig(text, source) {
|
|
|
109
153
|
...(typeof p.baseUrl === "string" ? { baseUrl: p.baseUrl } : {}),
|
|
110
154
|
...(typeof p.promptCaching === "boolean" ? { promptCaching: p.promptCaching } : {}),
|
|
111
155
|
...(typeof p.streamIdleMs === "number" ? { streamIdleMs: p.streamIdleMs } : {}),
|
|
156
|
+
...(typeof p.contextWindow === "number" ? { contextWindow: p.contextWindow } : {}),
|
|
112
157
|
};
|
|
113
158
|
}
|
|
114
159
|
out.models = models;
|