@giovannijecha/jecode 0.5.0 → 0.7.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 +34 -6
- package/dist/batch.js +59 -5
- package/dist/cli-info.js +3 -0
- package/dist/commands.js +38 -10
- package/dist/config.js +12 -0
- package/dist/context/capacity.js +18 -0
- package/dist/context/compactor.js +62 -0
- package/dist/context/manual.js +69 -0
- package/dist/context/policy.js +125 -0
- package/dist/context/projection.js +49 -0
- package/dist/controller-request.js +34 -0
- package/dist/controller.js +25 -18
- package/dist/conversation.js +31 -3
- package/dist/providers/anthropic.js +33 -3
- package/dist/providers/catalog.js +11 -4
- package/dist/providers/ollama.js +75 -1
- package/dist/providers/openai-codex.js +46 -2
- package/dist/providers/openai.js +17 -0
- package/dist/sessions/codec.js +39 -7
- package/dist/sessions/store.js +5 -5
- package/dist/settings-command.js +42 -2
- package/dist/settings.js +9 -0
- package/dist/timeline.js +90 -0
- package/dist/tui/app-state.js +1 -0
- package/dist/tui/app-workflows.js +105 -8
- package/dist/tui/app.js +14 -7
- package/dist/tui/components/misc.js +4 -2
- package/dist/usage.js +8 -0
- package/package.json +1 -1
package/dist/sessions/store.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as path from "node:path";
|
|
|
9
9
|
import { atomicWrite } from "../atomic.js";
|
|
10
10
|
import { CONVERSATION_LIMITS, ConversationTree } from "../conversation.js";
|
|
11
11
|
import { userDataPath } from "../user-data.js";
|
|
12
|
-
import { decodeHead, decodeMeta, decodeNode, encodeHead, encodeMeta, encodeNode, } from "./codec.js";
|
|
12
|
+
import { decodeHead, decodeMeta, decodeNode, encodeHead, encodeMeta, encodeNode, SESSION_SCHEMA, } from "./codec.js";
|
|
13
13
|
import { leaseOwner, leaseToken, pidIsAlive, removeLease, sessionLease, } from "./lease.js";
|
|
14
14
|
const DIRECTORY_MODE = 0o700;
|
|
15
15
|
const FILE_MODE = 0o600;
|
|
@@ -104,7 +104,7 @@ export class DurableSessionStore {
|
|
|
104
104
|
throw new Error("session checkpoint cannot be recovered safely");
|
|
105
105
|
}
|
|
106
106
|
head = {
|
|
107
|
-
version:
|
|
107
|
+
version: SESSION_SCHEMA,
|
|
108
108
|
sequence: candidate.sequence,
|
|
109
109
|
nodeId: candidate.node.id,
|
|
110
110
|
parentId: candidate.node.parentId,
|
|
@@ -128,14 +128,14 @@ export class DurableSessionStore {
|
|
|
128
128
|
const temporary = path.join(this.#bucket, `.${id}.${randomUUID()}.tmp`);
|
|
129
129
|
const target = this.#sessionDirectory(id);
|
|
130
130
|
const meta = {
|
|
131
|
-
version:
|
|
131
|
+
version: SESSION_SCHEMA,
|
|
132
132
|
id,
|
|
133
133
|
workspaceRoot: this.workspaceRoot,
|
|
134
134
|
workspaceDigest: this.workspaceDigest,
|
|
135
135
|
createdAt: now,
|
|
136
136
|
};
|
|
137
137
|
const head = {
|
|
138
|
-
version:
|
|
138
|
+
version: SESSION_SCHEMA,
|
|
139
139
|
sequence: conversation.nodes.length,
|
|
140
140
|
nodeId: active.id,
|
|
141
141
|
parentId: active.parentId,
|
|
@@ -187,7 +187,7 @@ export class DurableSessionStore {
|
|
|
187
187
|
assertSharedNodes(previous.conversation, conversation, replacesHead ? active.id : undefined);
|
|
188
188
|
const now = new Date().toISOString();
|
|
189
189
|
const head = {
|
|
190
|
-
version:
|
|
190
|
+
version: SESSION_SCHEMA,
|
|
191
191
|
sequence: previous.head.sequence + 1,
|
|
192
192
|
nodeId: active.id,
|
|
193
193
|
parentId: active.parentId,
|
package/dist/settings-command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The persistent settings hub. Provider access and model selection reuse the
|
|
2
2
|
// same command flows exposed directly through /providers and /models.
|
|
3
3
|
import { saveCommandSettings } from "./command-settings.js";
|
|
4
|
+
import { MAX_COMPACTION_PERCENT, MIN_COMPACTION_PERCENT } from "./context/policy.js";
|
|
4
5
|
import { modelsCommand } from "./model-command.js";
|
|
5
6
|
import { providerFailure } from "./provider-errors.js";
|
|
6
7
|
import { providerLabel } from "./provider-label.js";
|
|
@@ -43,6 +44,9 @@ export async function settingsCommand(session, host) {
|
|
|
43
44
|
case "maxSteps":
|
|
44
45
|
await numberSetting(session, host, "maxSteps", "max tool steps");
|
|
45
46
|
break;
|
|
47
|
+
case "compactionPercent":
|
|
48
|
+
await compactionSetting(session, host);
|
|
49
|
+
break;
|
|
46
50
|
case "reducedMotion":
|
|
47
51
|
await motionSetting(session, host);
|
|
48
52
|
break;
|
|
@@ -53,9 +57,11 @@ export async function settingsCommand(session, host) {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
export function settingsPicker(values, index = 0) {
|
|
60
|
+
const items = settingsItems(values);
|
|
56
61
|
return {
|
|
57
62
|
title: [],
|
|
58
|
-
options:
|
|
63
|
+
options: items.map((item) => item.option),
|
|
64
|
+
visible: items.length,
|
|
59
65
|
index,
|
|
60
66
|
};
|
|
61
67
|
}
|
|
@@ -76,13 +82,17 @@ function settingsItems(values) {
|
|
|
76
82
|
option: { label: "max output tokens", value: String(values.maxTokens) },
|
|
77
83
|
}]),
|
|
78
84
|
{ action: "maxSteps", option: { label: "max tool steps", value: String(values.maxSteps) } },
|
|
85
|
+
{
|
|
86
|
+
action: "compactionPercent",
|
|
87
|
+
option: { label: "context compaction", value: `${values.compactionPercent}%` },
|
|
88
|
+
},
|
|
79
89
|
{
|
|
80
90
|
action: "reducedMotion",
|
|
81
91
|
option: { label: "reduced motion", value: values.reducedMotion ? "on" : "off" },
|
|
82
92
|
},
|
|
83
93
|
{
|
|
84
94
|
action: "providers",
|
|
85
|
-
option: { label: "providers", hint: "manage
|
|
95
|
+
option: { label: "providers", hint: "manage connections" },
|
|
86
96
|
},
|
|
87
97
|
];
|
|
88
98
|
}
|
|
@@ -93,6 +103,7 @@ function settingsValues(session) {
|
|
|
93
103
|
effort: session.config.effort,
|
|
94
104
|
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
95
105
|
maxSteps: session.config.maxSteps,
|
|
106
|
+
compactionPercent: session.config.compactionPercent,
|
|
96
107
|
reducedMotion: session.config.reducedMotion,
|
|
97
108
|
};
|
|
98
109
|
}
|
|
@@ -185,6 +196,35 @@ async function numberSetting(session, host, name, label) {
|
|
|
185
196
|
return;
|
|
186
197
|
session.config[name] = value;
|
|
187
198
|
}
|
|
199
|
+
async function compactionSetting(session, host) {
|
|
200
|
+
if (host.type === undefined)
|
|
201
|
+
return;
|
|
202
|
+
const label = "context compaction";
|
|
203
|
+
const field = {
|
|
204
|
+
title: heading(label, `${MIN_COMPACTION_PERCENT}-${MAX_COMPACTION_PERCENT} percent`, session.palette),
|
|
205
|
+
right: "enter save · esc back",
|
|
206
|
+
editor: of(String(session.config.compactionPercent)),
|
|
207
|
+
secret: false,
|
|
208
|
+
note: "Compacts when model context reaches this percentage.",
|
|
209
|
+
};
|
|
210
|
+
const text = await host.type(field);
|
|
211
|
+
if (text === undefined)
|
|
212
|
+
return;
|
|
213
|
+
const value = Number(text);
|
|
214
|
+
if (!Number.isSafeInteger(value) ||
|
|
215
|
+
value < MIN_COMPACTION_PERCENT ||
|
|
216
|
+
value > MAX_COMPACTION_PERCENT) {
|
|
217
|
+
host.emit({
|
|
218
|
+
kind: "notice",
|
|
219
|
+
text: `${label} must be from ${MIN_COMPACTION_PERCENT} to ${MAX_COMPACTION_PERCENT}`,
|
|
220
|
+
tone: "error",
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!(await saveCommandSettings(host, { compactionPercent: value })))
|
|
225
|
+
return;
|
|
226
|
+
session.config.compactionPercent = value;
|
|
227
|
+
}
|
|
188
228
|
function chooser(host) {
|
|
189
229
|
if (host.choose === undefined) {
|
|
190
230
|
host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
|
package/dist/settings.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { chmod, mkdir } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { atomicWrite } from "./atomic.js";
|
|
6
|
+
import { MAX_COMPACTION_PERCENT, MIN_COMPACTION_PERCENT } from "./context/policy.js";
|
|
6
7
|
import { EFFORTS } from "./effort.js";
|
|
7
8
|
import { providerNames } from "./providers/index.js";
|
|
8
9
|
import { parseOllamaEndpoint } from "./providers/ollama-endpoint.js";
|
|
@@ -59,6 +60,7 @@ function normalize(value) {
|
|
|
59
60
|
const reducedMotion = typeof value["reducedMotion"] === "boolean" ? value["reducedMotion"] : undefined;
|
|
60
61
|
const maxTokens = positiveInteger(value["maxTokens"]);
|
|
61
62
|
const maxSteps = positiveInteger(value["maxSteps"]);
|
|
63
|
+
const compactionPercent = percentage(value["compactionPercent"]);
|
|
62
64
|
return {
|
|
63
65
|
...(provider === undefined ? {} : { provider }),
|
|
64
66
|
...(models === undefined ? {} : { models }),
|
|
@@ -67,6 +69,7 @@ function normalize(value) {
|
|
|
67
69
|
...(reducedMotion === undefined ? {} : { reducedMotion }),
|
|
68
70
|
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
69
71
|
...(maxSteps === undefined ? {} : { maxSteps }),
|
|
72
|
+
...(compactionPercent === undefined ? {} : { compactionPercent }),
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
function modelsOf(value, providers) {
|
|
@@ -91,6 +94,12 @@ function endpoint(value) {
|
|
|
91
94
|
function positiveInteger(value) {
|
|
92
95
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
93
96
|
}
|
|
97
|
+
function percentage(value) {
|
|
98
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
99
|
+
value >= MIN_COMPACTION_PERCENT && value <= MAX_COMPACTION_PERCENT
|
|
100
|
+
? value
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
94
103
|
function record(value) {
|
|
95
104
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
105
|
}
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A compact, read-only projection of the durable conversation tree.
|
|
2
|
+
//
|
|
3
|
+
// Selecting a node changes only the in-memory path. The next real user turn
|
|
4
|
+
// is what persists a branch, so opening or cancelling this control plane can
|
|
5
|
+
// never create empty history.
|
|
6
|
+
import { usageFromHistory } from "./usage.js";
|
|
7
|
+
import { heading } from "./tui/picker.js";
|
|
8
|
+
export function timelinePicker(conversation, palette) {
|
|
9
|
+
const entries = timelineEntries(conversation);
|
|
10
|
+
const selectedId = conversation.latestCompleted()?.activeNodeId ?? 0;
|
|
11
|
+
const index = Math.max(0, entries.findIndex((entry) => entry.node.id === selectedId));
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
picker: {
|
|
14
|
+
title: heading("timeline", "conversation tree", palette),
|
|
15
|
+
searchable: true,
|
|
16
|
+
query: "",
|
|
17
|
+
visible: 8,
|
|
18
|
+
options: entries.map((entry) => ({
|
|
19
|
+
label: `${entry.prefix}${preview(entry.node)}`,
|
|
20
|
+
hint: stamp(entry.node.createdAt),
|
|
21
|
+
...(entry.node.id === selectedId ? { value: "active" } : {}),
|
|
22
|
+
})),
|
|
23
|
+
index,
|
|
24
|
+
},
|
|
25
|
+
nodeIds: Object.freeze(entries.map((entry) => entry.node.id)),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function selectTimeline(session, choose) {
|
|
29
|
+
const timeline = timelinePicker(session.conversation, session.palette);
|
|
30
|
+
if (timeline.nodeIds.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
const index = await choose(timeline.picker);
|
|
33
|
+
const nodeId = index === undefined ? undefined : timeline.nodeIds[index];
|
|
34
|
+
if (nodeId === undefined || nodeId === session.conversation.activeNodeId)
|
|
35
|
+
return false;
|
|
36
|
+
session.conversation = session.conversation.select(nodeId);
|
|
37
|
+
session.usage = usageFromHistory(session.conversation.history);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function timelineEntries(conversation) {
|
|
41
|
+
const completed = conversation.nodes.filter((node) => node.settlement === "completed");
|
|
42
|
+
const completedIds = new Set(completed.map((node) => node.id));
|
|
43
|
+
const children = new Map();
|
|
44
|
+
for (const node of completed) {
|
|
45
|
+
let parentId = node.parentId;
|
|
46
|
+
while (parentId !== 0 && !completedIds.has(parentId)) {
|
|
47
|
+
parentId = conversation.node(parentId)?.parentId ?? 0;
|
|
48
|
+
}
|
|
49
|
+
const siblings = children.get(parentId) ?? [];
|
|
50
|
+
siblings.push(node);
|
|
51
|
+
children.set(parentId, siblings);
|
|
52
|
+
}
|
|
53
|
+
const entries = [];
|
|
54
|
+
const visit = (parentId, lanes) => {
|
|
55
|
+
const siblings = children.get(parentId) ?? [];
|
|
56
|
+
for (let index = 0; index < siblings.length; index++) {
|
|
57
|
+
const node = siblings[index];
|
|
58
|
+
const forks = siblings.length > 1;
|
|
59
|
+
const last = index === siblings.length - 1;
|
|
60
|
+
const hidden = Math.max(0, lanes.length - 4);
|
|
61
|
+
const lanePrefix = `${hidden === 0 ? "" : "… "}${lanes.slice(hidden)
|
|
62
|
+
.map((closed) => closed ? " " : "│ ").join("")}`;
|
|
63
|
+
entries.push({
|
|
64
|
+
node,
|
|
65
|
+
prefix: `${lanePrefix}${forks ? (last ? "└─ " : "├─ ") : "• "}`,
|
|
66
|
+
});
|
|
67
|
+
visit(node.id, forks ? [...lanes, last] : lanes);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
visit(0, []);
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
function preview(node) {
|
|
74
|
+
for (const message of node.messages) {
|
|
75
|
+
if (message.role !== "user")
|
|
76
|
+
continue;
|
|
77
|
+
const text = message.content.find((block) => block.kind === "text")?.text
|
|
78
|
+
.replace(/\s+/gu, " ").trim();
|
|
79
|
+
if (text !== undefined && text !== "")
|
|
80
|
+
return text.slice(0, 160);
|
|
81
|
+
}
|
|
82
|
+
return "Untitled turn";
|
|
83
|
+
}
|
|
84
|
+
function stamp(value) {
|
|
85
|
+
const date = new Date(value);
|
|
86
|
+
if (!Number.isFinite(date.getTime()))
|
|
87
|
+
return value.replace("T", " ").slice(0, 16);
|
|
88
|
+
const two = (part) => String(part).padStart(2, "0");
|
|
89
|
+
return `${two(date.getHours())}:${two(date.getMinutes())}`;
|
|
90
|
+
}
|
package/dist/tui/app-state.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// Foreground command and model-turn workflows for the TUI shell.
|
|
2
2
|
import { handleCommand } from "../commands.js";
|
|
3
3
|
import { runTurn } from "../controller.js";
|
|
4
|
+
import { resolveContextPolicy } from "../context/capacity.js";
|
|
5
|
+
import { compactContext } from "../context/compactor.js";
|
|
6
|
+
import { compactSession } from "../context/manual.js";
|
|
7
|
+
import { isContextOverflow, shouldResolveContextPolicy } from "../context/policy.js";
|
|
4
8
|
import { updateSettings } from "../settings.js";
|
|
5
9
|
import { saveTranscript } from "../transcript-export.js";
|
|
6
|
-
import { recordUsage } from "../usage.js";
|
|
10
|
+
import { recordAuxiliaryUsage, recordUsage } from "../usage.js";
|
|
11
|
+
import { selectTimeline } from "../timeline.js";
|
|
7
12
|
import { answerAt } from "./approve.js";
|
|
8
13
|
import { cancel as cancelOpen } from "./overlay.js";
|
|
9
14
|
import { controllerOptions, turnFailure } from "./session-view.js";
|
|
@@ -11,6 +16,10 @@ import { transcribe } from "./turn.js";
|
|
|
11
16
|
const WAITING = "Waiting";
|
|
12
17
|
export function appWorkflows(options) {
|
|
13
18
|
const { session, state, permissions, feedback } = options;
|
|
19
|
+
const choose = (picker) => new Promise((resolve) => {
|
|
20
|
+
state.open = { picker, settle: resolve };
|
|
21
|
+
options.render();
|
|
22
|
+
});
|
|
14
23
|
async function command(text) {
|
|
15
24
|
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
16
25
|
if (activity === undefined)
|
|
@@ -23,10 +32,7 @@ export function appWorkflows(options) {
|
|
|
23
32
|
state.open = { help: true, settle: resolve };
|
|
24
33
|
options.render();
|
|
25
34
|
}),
|
|
26
|
-
choose
|
|
27
|
-
state.open = { picker, settle: resolve };
|
|
28
|
-
options.render();
|
|
29
|
-
}),
|
|
35
|
+
choose,
|
|
30
36
|
dismiss: () => {
|
|
31
37
|
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
32
38
|
options.render();
|
|
@@ -48,6 +54,7 @@ export function appWorkflows(options) {
|
|
|
48
54
|
state.follow = true;
|
|
49
55
|
state.unseen = 0;
|
|
50
56
|
state.lastMaxScroll = 0;
|
|
57
|
+
state.committedNodeId = 0;
|
|
51
58
|
},
|
|
52
59
|
permissions,
|
|
53
60
|
exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
|
|
@@ -55,6 +62,27 @@ export function appWorkflows(options) {
|
|
|
55
62
|
await updateSettings(patch);
|
|
56
63
|
},
|
|
57
64
|
refreshSettings: options.refreshSettings,
|
|
65
|
+
timeline: async () => {
|
|
66
|
+
const selected = await selectTimeline(session, choose);
|
|
67
|
+
if (!selected)
|
|
68
|
+
return "unchanged";
|
|
69
|
+
options.replaceTranscript();
|
|
70
|
+
return session.conversation.activeNodeId === state.committedNodeId
|
|
71
|
+
? "unchanged"
|
|
72
|
+
: "selected";
|
|
73
|
+
},
|
|
74
|
+
compact: async () => {
|
|
75
|
+
if (session.conversation.activeNodeId !== state.committedNodeId) {
|
|
76
|
+
return "branch-pending";
|
|
77
|
+
}
|
|
78
|
+
return compactSession(session, {
|
|
79
|
+
signal: activity.control.signal,
|
|
80
|
+
onStatus: (status) => {
|
|
81
|
+
state.status = status ?? activity.label;
|
|
82
|
+
options.render();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
58
86
|
});
|
|
59
87
|
if (outcome === "exit")
|
|
60
88
|
state.closeWhenIdle = true;
|
|
@@ -81,12 +109,18 @@ export function appWorkflows(options) {
|
|
|
81
109
|
return;
|
|
82
110
|
const parentId = session.conversation.activeNodeId;
|
|
83
111
|
const history = session.conversation.history;
|
|
112
|
+
const modelHistory = session.conversation.contextHistory;
|
|
84
113
|
const historyStart = history.length;
|
|
85
114
|
const blockStart = state.blocks.length;
|
|
86
115
|
const createdAt = new Date().toISOString();
|
|
116
|
+
const prospectiveNodeId = session.conversation.nodes.length + 1;
|
|
87
117
|
let nodeId;
|
|
118
|
+
let context;
|
|
119
|
+
let contextPolicy;
|
|
88
120
|
options.emit({ kind: "user", text });
|
|
89
|
-
|
|
121
|
+
const user = { role: "user", content: [{ kind: "text", text }] };
|
|
122
|
+
history.push(user);
|
|
123
|
+
modelHistory.push(structuredClone(user));
|
|
90
124
|
const events = transcribe({
|
|
91
125
|
emit: options.emit,
|
|
92
126
|
render: options.render,
|
|
@@ -102,7 +136,7 @@ export function appWorkflows(options) {
|
|
|
102
136
|
},
|
|
103
137
|
usage: (usage) => recordUsage(session.usage, usage),
|
|
104
138
|
});
|
|
105
|
-
|
|
139
|
+
const persist = async (checkpoint, settlement) => {
|
|
106
140
|
const next = session.conversation.commit({
|
|
107
141
|
...(nodeId === undefined ? {} : { nodeId }),
|
|
108
142
|
parentId,
|
|
@@ -114,14 +148,77 @@ export function appWorkflows(options) {
|
|
|
114
148
|
},
|
|
115
149
|
messages: checkpoint.slice(historyStart),
|
|
116
150
|
blocks: state.blocks.slice(blockStart),
|
|
151
|
+
...(context === undefined ? {} : { context }),
|
|
117
152
|
}, settlement);
|
|
118
153
|
await session.persistence?.checkpoint(next);
|
|
119
154
|
session.conversation = next;
|
|
120
155
|
nodeId = next.activeNodeId;
|
|
156
|
+
state.committedNodeId = next.activeNodeId;
|
|
157
|
+
};
|
|
158
|
+
const compact = async (checkpoint, projected, reason, error) => {
|
|
159
|
+
if (reason === "overflow" && (error === undefined || !isContextOverflow(error))) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const force = reason === "overflow";
|
|
163
|
+
if (!shouldResolveContextPolicy(projected, session.usage.lastInputTokens, force)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (contextPolicy === undefined) {
|
|
167
|
+
state.status = "Checking context";
|
|
168
|
+
options.render();
|
|
169
|
+
contextPolicy = resolveContextPolicy({
|
|
170
|
+
provider: session.provider,
|
|
171
|
+
model: session.model,
|
|
172
|
+
compactionPercent: session.config.compactionPercent,
|
|
173
|
+
signal: activity.control.signal,
|
|
174
|
+
onStatus: (status) => {
|
|
175
|
+
state.status = status;
|
|
176
|
+
options.render();
|
|
177
|
+
},
|
|
178
|
+
}).finally(() => {
|
|
179
|
+
state.status = WAITING;
|
|
180
|
+
options.render();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const result = await compactContext({
|
|
184
|
+
provider: session.provider,
|
|
185
|
+
model: session.model,
|
|
186
|
+
effort: session.config.effort,
|
|
187
|
+
context: projected,
|
|
188
|
+
turn: checkpoint.slice(historyStart),
|
|
189
|
+
nodeId: nodeId ?? prospectiveNodeId,
|
|
190
|
+
coveredMessages: context?.messageCount ?? 0,
|
|
191
|
+
lastInputTokens: session.usage.lastInputTokens,
|
|
192
|
+
signal: activity.control.signal,
|
|
193
|
+
force,
|
|
194
|
+
policy: await contextPolicy,
|
|
195
|
+
onBegin: () => {
|
|
196
|
+
state.status = "Compacting";
|
|
197
|
+
options.render();
|
|
198
|
+
},
|
|
199
|
+
onEnd: () => {
|
|
200
|
+
state.status = WAITING;
|
|
201
|
+
options.render();
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
if (result === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
context = result.anchor;
|
|
207
|
+
if (result.usage !== undefined)
|
|
208
|
+
recordAuxiliaryUsage(session.usage, result.usage);
|
|
209
|
+
return result.messages;
|
|
210
|
+
};
|
|
211
|
+
events.onContext = compact;
|
|
212
|
+
events.onCheckpoint = async (checkpoint, settlement, projected) => {
|
|
213
|
+
await persist(checkpoint, settlement);
|
|
214
|
+
const compacted = await compact(checkpoint, projected, "budget");
|
|
215
|
+
if (compacted !== undefined)
|
|
216
|
+
await persist(checkpoint, settlement);
|
|
217
|
+
return compacted;
|
|
121
218
|
};
|
|
122
219
|
let finishReason;
|
|
123
220
|
try {
|
|
124
|
-
await runTurn(history, controllerOptions(session, permissions.availableTools()), events, activity.control.signal);
|
|
221
|
+
await runTurn(history, controllerOptions(session, permissions.availableTools()), events, activity.control.signal, modelHistory);
|
|
125
222
|
}
|
|
126
223
|
catch (error) {
|
|
127
224
|
const interrupted = activity.control.signal.aborted;
|
package/dist/tui/app.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
32
32
|
const workspace = await workspaceLabel(session.config.root);
|
|
33
33
|
const state = appState();
|
|
34
34
|
state.blocks.push(...session.conversation.transcript);
|
|
35
|
+
state.committedNodeId = session.conversation.activeNodeId;
|
|
35
36
|
const permissions = sessionPermissions(session.tools, session.config.autoApprove);
|
|
36
37
|
let closed;
|
|
37
38
|
let frameTimer;
|
|
@@ -116,6 +117,16 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
116
117
|
if (state.follow)
|
|
117
118
|
state.unseen = 0;
|
|
118
119
|
};
|
|
120
|
+
const replaceTranscript = () => {
|
|
121
|
+
state.blocks.splice(0, state.blocks.length, ...session.conversation.transcript);
|
|
122
|
+
state.scroll = 0;
|
|
123
|
+
state.follow = true;
|
|
124
|
+
state.unseen = 0;
|
|
125
|
+
state.lastMaxScroll = 0;
|
|
126
|
+
transcript.invalidate();
|
|
127
|
+
paint.invalidate();
|
|
128
|
+
render();
|
|
129
|
+
};
|
|
119
130
|
function quit() {
|
|
120
131
|
if (!live)
|
|
121
132
|
return;
|
|
@@ -187,6 +198,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
187
198
|
emit,
|
|
188
199
|
commandNotice,
|
|
189
200
|
render,
|
|
201
|
+
replaceTranscript,
|
|
190
202
|
refreshSettings: () => {
|
|
191
203
|
terminal.setReducedMotion(session.config.reducedMotion);
|
|
192
204
|
paint.invalidate();
|
|
@@ -229,14 +241,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
229
241
|
return;
|
|
230
242
|
try {
|
|
231
243
|
await launch.open(candidate.id);
|
|
232
|
-
|
|
244
|
+
replaceTranscript();
|
|
245
|
+
state.committedNodeId = session.conversation.activeNodeId;
|
|
233
246
|
state.past.length = 0;
|
|
234
|
-
state.scroll = 0;
|
|
235
|
-
state.follow = true;
|
|
236
|
-
state.unseen = 0;
|
|
237
|
-
state.lastMaxScroll = 0;
|
|
238
|
-
transcript.invalidate();
|
|
239
|
-
paint.invalidate();
|
|
240
247
|
finishActivity(activity);
|
|
241
248
|
return;
|
|
242
249
|
}
|
|
@@ -5,11 +5,13 @@ export function renderNotice(block, width, pal) {
|
|
|
5
5
|
warn: pal.ink.attention,
|
|
6
6
|
error: pal.ink.removed,
|
|
7
7
|
};
|
|
8
|
-
const mark = block.tone === "error" ? "× " : block.tone === "warn" ? "! " :
|
|
8
|
+
const mark = block.tone === "error" ? "× " : block.tone === "warn" ? "! " : undefined;
|
|
9
9
|
return [
|
|
10
10
|
"",
|
|
11
11
|
...wrap(block.text, Math.max(1, width - 3)).map((line, index) => row(width, [
|
|
12
|
-
|
|
12
|
+
...(mark === undefined
|
|
13
|
+
? []
|
|
14
|
+
: [{ text: index === 0 ? mark : " ", fg: fg[block.tone], bold: index === 0 }]),
|
|
13
15
|
{ text: line, fg: fg[block.tone] },
|
|
14
16
|
], [], undefined, 1)),
|
|
15
17
|
];
|
package/dist/usage.js
CHANGED
|
@@ -13,6 +13,14 @@ export function emptyUsage() {
|
|
|
13
13
|
export function recordUsage(total, next) {
|
|
14
14
|
total.requests += 1;
|
|
15
15
|
total.lastInputTokens = next.inputTokens;
|
|
16
|
+
addUsage(total, next);
|
|
17
|
+
}
|
|
18
|
+
/** Account for an internal request without replacing the main context signal. */
|
|
19
|
+
export function recordAuxiliaryUsage(total, next) {
|
|
20
|
+
total.requests += 1;
|
|
21
|
+
addUsage(total, next);
|
|
22
|
+
}
|
|
23
|
+
function addUsage(total, next) {
|
|
16
24
|
total.inputTokens += next.inputTokens;
|
|
17
25
|
total.outputTokens += next.outputTokens;
|
|
18
26
|
total.cachedInputTokens += next.cachedInputTokens;
|