@gaunt-sloth/agent 2.0.0-beta.5 → 2.0.0-beta.6
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 +6 -1
- package/cli.js +93 -6
- package/dist/middleware/binaryContentInjectionMiddleware.js +6 -1
- package/dist/middleware/binaryContentInjectionMiddleware.js.map +1 -1
- package/dist/middleware/frontendImageInjectionMiddleware.d.ts +52 -25
- package/dist/middleware/frontendImageInjectionMiddleware.js +91 -13
- package/dist/middleware/frontendImageInjectionMiddleware.js.map +1 -1
- package/dist/middleware/registry.js +14 -12
- package/dist/middleware/registry.js.map +1 -1
- package/dist/modules/acp/acpAgentApp.js +5 -0
- package/dist/modules/acp/acpAgentApp.js.map +1 -1
- package/dist/modules/acp/acpAgentAppV1.js +5 -0
- package/dist/modules/acp/acpAgentAppV1.js.map +1 -1
- package/dist/modules/acp/acpCommon.d.ts +6 -14
- package/dist/modules/acp/acpCommon.js +6 -16
- package/dist/modules/acp/acpCommon.js.map +1 -1
- package/dist/modules/apiAgUiModule.js +7 -0
- package/dist/modules/apiAgUiModule.js.map +1 -1
- package/dist/modules/interactiveSessionModule.d.ts +10 -1
- package/dist/modules/interactiveSessionModule.js +224 -41
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/modules/sessionResume.d.ts +123 -0
- package/dist/modules/sessionResume.js +268 -0
- package/dist/modules/sessionResume.js.map +1 -0
- package/dist/modules/slashCommands.d.ts +142 -0
- package/dist/modules/slashCommands.js +352 -15
- package/dist/modules/slashCommands.js.map +1 -1
- package/dist/modules/workspace.d.ts +24 -0
- package/dist/modules/workspace.js +27 -0
- package/dist/modules/workspace.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-20 — **the one seam through which a session re-enters a stored conversation.**
|
|
4
|
+
*
|
|
5
|
+
* Three spellings reach it — `--resume <id>` on `gth chat` / `gth code` / bare `gth`, `/resume <id>`
|
|
6
|
+
* inside a running session, and `gth history resume <id>` — and every one of them is exactly two
|
|
7
|
+
* calls: {@link resolveResumeTarget} decides whether the conversation CAN be resumed and gathers what
|
|
8
|
+
* a resume needs, and {@link applyResumeTarget} makes the session be in it. Neither surface (the
|
|
9
|
+
* readline loop, the Ink TUI) knows anything a resume requires beyond those two calls, so a check
|
|
10
|
+
* added here is applied to every spelling, and a spelling cannot drift into meaning something else.
|
|
11
|
+
*
|
|
12
|
+
* The seam sits here, in the agent package, rather than wholly on `GthAgentRunner`, because half of
|
|
13
|
+
* what a resume does is not the runner's: reading the history store, comparing workspaces and
|
|
14
|
+
* re-binding the recorder are session concerns the runner deliberately does not have. The runner
|
|
15
|
+
* owns the half that touches the graph — `GthAgentRunner.resumeConversation` rotates onto the
|
|
16
|
+
* stored thread and installs the conversation's grants — and this module owns the checks and the
|
|
17
|
+
* recorder. Both halves are exercised by both spellings, so breaking either breaks both.
|
|
18
|
+
*
|
|
19
|
+
* **The checks run in a fixed order, and each refusal has its own sentence**, because the person
|
|
20
|
+
* typing an id needs to know which of five different things is wrong, and "cannot resume" tells
|
|
21
|
+
* them none of it:
|
|
22
|
+
*
|
|
23
|
+
* 1. history is off — the one switch that governs recording, checkpointing and resuming alike;
|
|
24
|
+
* 2. the store did not open — asked for and not available, which is a different fact from off;
|
|
25
|
+
* 3. no such conversation;
|
|
26
|
+
* 4. the conversation exists and is not resumable — never had a thread, lost it, or its thread was
|
|
27
|
+
* never checkpointed — with the reason class where it is known;
|
|
28
|
+
* 5. it was recorded in another directory — the same comparison ACP's `session/new` makes.
|
|
29
|
+
*/
|
|
30
|
+
import { resolve } from 'node:path';
|
|
31
|
+
import { isHistoryEnabled, } from '@gaunt-sloth/core/history/historyEnabled.js';
|
|
32
|
+
import { listResumableConversationsSafe, lookupConversationSafe, } from '@gaunt-sloth/core/history/recordSession.js';
|
|
33
|
+
import { formatConversationList } from '@gaunt-sloth/core/history/historyFormat.js';
|
|
34
|
+
import { loadConversationGrantsSafe, } from '@gaunt-sloth/core/core/approvals/conversationGrants.js';
|
|
35
|
+
import { isSameWorkspace } from '#src/modules/workspace.js';
|
|
36
|
+
/** The commands whose sessions checkpoint a thread; every other command's rows are single-shot. */
|
|
37
|
+
const INTERACTIVE_COMMANDS = new Set(['chat', 'code']);
|
|
38
|
+
/**
|
|
39
|
+
* Decide whether conversation `id` can be resumed by this session, and gather what the resume
|
|
40
|
+
* needs. Never throws: a failure to read is a refusal with a reason, never a crash.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveResumeTarget(session, id) {
|
|
43
|
+
if (!isHistoryEnabled(session.config))
|
|
44
|
+
return refuse({ kind: 'history-off' });
|
|
45
|
+
// Asked for and not available: the store did not open, so nothing below could be read anyway,
|
|
46
|
+
// and calling the id "unknown" would blame the person for the disk.
|
|
47
|
+
if (!session.checkpointer.durable)
|
|
48
|
+
return refuse({ kind: 'store-unavailable' });
|
|
49
|
+
const stored = lookupConversationSafe(session.config, id);
|
|
50
|
+
if (!stored)
|
|
51
|
+
return refuse({ kind: 'unknown', id });
|
|
52
|
+
const { summary, turns } = stored;
|
|
53
|
+
if (!summary.threadId) {
|
|
54
|
+
const reason = INTERACTIVE_COMMANDS.has(summary.command ?? '') ? 'no-thread' : 'single-shot';
|
|
55
|
+
return refuse({ kind: 'not-resumable', id, reason, command: summary.command });
|
|
56
|
+
}
|
|
57
|
+
// A thread with no checkpoint is refused exactly like a null thread: there is no state to
|
|
58
|
+
// re-enter, and driving the graph on it would silently start a fresh conversation under an old
|
|
59
|
+
// id — which is the one thing a resume must never do.
|
|
60
|
+
let checkpointed;
|
|
61
|
+
try {
|
|
62
|
+
const tuple = await session.checkpointer.saver.getTuple({
|
|
63
|
+
configurable: { thread_id: summary.threadId },
|
|
64
|
+
});
|
|
65
|
+
checkpointed = tuple !== undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return refuse({ kind: 'not-resumable', id, reason: 'unreadable', command: summary.command });
|
|
69
|
+
}
|
|
70
|
+
if (!checkpointed) {
|
|
71
|
+
return refuse({ kind: 'not-resumable', id, reason: 'no-checkpoint', command: summary.command });
|
|
72
|
+
}
|
|
73
|
+
// The same comparison ACP's `session/new` makes, on resolved paths. A row with no project on
|
|
74
|
+
// record has nothing to compare and proceeds: the check refuses a KNOWN mismatch, and a datum
|
|
75
|
+
// that was never written is not one.
|
|
76
|
+
if (summary.project) {
|
|
77
|
+
const stored = resolve(summary.project);
|
|
78
|
+
const current = resolve(session.workspace);
|
|
79
|
+
if (!isSameWorkspace(stored, current)) {
|
|
80
|
+
return refuse({ kind: 'workspace-mismatch', id, stored, current });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
target: {
|
|
86
|
+
conversationId: id,
|
|
87
|
+
threadId: summary.threadId,
|
|
88
|
+
summary,
|
|
89
|
+
turns,
|
|
90
|
+
grants: loadConversationGrantsSafe(session.config, id),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const refuse = (refusal) => ({ ok: false, refusal });
|
|
95
|
+
/**
|
|
96
|
+
* Make the session BE in the resolved conversation: the runner rotates onto the stored thread and
|
|
97
|
+
* installs the conversation's grants, and the checkpointer is told which row to mark if a write
|
|
98
|
+
* fails from here on. The caller switches its own recorder id to `target.conversationId`, because
|
|
99
|
+
* that id is the caller's variable; nothing else is needed.
|
|
100
|
+
*
|
|
101
|
+
* Rejects when the runner refuses — a turn still running, or a tool approval still pending on the
|
|
102
|
+
* thread being left — and then nothing has moved: the checkpointer is bound only after the runner
|
|
103
|
+
* has actually rotated, so a refused resume leaves the session exactly where it was.
|
|
104
|
+
*/
|
|
105
|
+
export async function applyResumeTarget(session, target) {
|
|
106
|
+
await session.runner.resumeConversation({ threadId: target.threadId, grants: target.grants });
|
|
107
|
+
session.checkpointer.bindConversation?.(target.conversationId);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The conversations a bare `/resume` offers: every resumable one except the conversation the
|
|
111
|
+
* session is already in. Fail-soft and empty when history is off or the store is absent.
|
|
112
|
+
*/
|
|
113
|
+
export function listResumeCandidates(config, currentConversationId, limit = 20) {
|
|
114
|
+
return listResumableConversationsSafe(config, { limit, exclude: currentConversationId });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse the id a person typed after `--resume`, `/resume` or `history resume`. A positive integer
|
|
118
|
+
* in decimal, or `null` — the ids `gth history list` prints are exactly that, and anything else is
|
|
119
|
+
* a typo worth naming rather than a lookup worth making.
|
|
120
|
+
*/
|
|
121
|
+
export function parseResumeId(raw) {
|
|
122
|
+
if (raw === undefined)
|
|
123
|
+
return null;
|
|
124
|
+
const trimmed = raw.trim().replace(/^#/, '');
|
|
125
|
+
if (!/^\d+$/.test(trimmed))
|
|
126
|
+
return null;
|
|
127
|
+
const id = Number.parseInt(trimmed, 10);
|
|
128
|
+
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
|
129
|
+
}
|
|
130
|
+
// ── Notices — shared by both surfaces so one refusal has one sentence. ────────────────────────────
|
|
131
|
+
const plural = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
132
|
+
/**
|
|
133
|
+
* The banner a resumed session shows first: which conversation, when it started, how much of it is
|
|
134
|
+
* on record, and under which command and model — the facts a person needs to confirm they picked
|
|
135
|
+
* the right id. It also states the one thing a resume changes that nothing on screen shows: the
|
|
136
|
+
* approvals granted in that conversation are in force again.
|
|
137
|
+
*/
|
|
138
|
+
export function resumedConversationNotice(target) {
|
|
139
|
+
const { summary } = target;
|
|
140
|
+
const recorded = `${plural(summary.turnCount, 'turn')} recorded under gth ${summary.command ?? 'chat'}`;
|
|
141
|
+
const model = summary.model ? `, with ${summary.model}` : '';
|
|
142
|
+
return {
|
|
143
|
+
title: `Resumed conversation #${target.conversationId}`,
|
|
144
|
+
lines: [
|
|
145
|
+
`Started ${summary.startedTs}${summary.project ? ` in ${summary.project}` : ''}.`,
|
|
146
|
+
`${recorded}${model}.`,
|
|
147
|
+
target.turns.length > 0
|
|
148
|
+
? 'The recorded turns are shown below; the model continues from where it left off.'
|
|
149
|
+
: 'No turns were recorded yet; the model continues from where it left off.',
|
|
150
|
+
'Approvals you granted in it are in force again, and new ones will be kept with it.',
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The sentence for each refusal. One title states what did not happen and why; the body says what
|
|
156
|
+
* to do instead. `inSession` picks the pointer: the slash commands a running session has, or the
|
|
157
|
+
* CLI commands a shell has.
|
|
158
|
+
*/
|
|
159
|
+
export function resumeRefusalNotice(refusal, options = {}) {
|
|
160
|
+
const list = options.inSession ? '/resume with no id' : '`gth history list`';
|
|
161
|
+
switch (refusal.kind) {
|
|
162
|
+
case 'history-off':
|
|
163
|
+
return {
|
|
164
|
+
title: 'Cannot resume: history is off',
|
|
165
|
+
lines: [
|
|
166
|
+
'Recording is on by default; `history.enabled: false` in your config turns it off, and ' +
|
|
167
|
+
'this session has it off. Only a recorded conversation can be resumed.',
|
|
168
|
+
'Remove that key, or set it to true, and start again.',
|
|
169
|
+
],
|
|
170
|
+
tone: 'warn',
|
|
171
|
+
};
|
|
172
|
+
case 'store-unavailable':
|
|
173
|
+
return {
|
|
174
|
+
title: 'Cannot resume: the conversation store did not open',
|
|
175
|
+
lines: [
|
|
176
|
+
'The history database could not be opened, so nothing can be read from it; the notice ' +
|
|
177
|
+
'about the store says why.',
|
|
178
|
+
'Nothing was changed.',
|
|
179
|
+
],
|
|
180
|
+
tone: 'warn',
|
|
181
|
+
};
|
|
182
|
+
case 'unknown':
|
|
183
|
+
return {
|
|
184
|
+
title: `No conversation #${refusal.id}`,
|
|
185
|
+
lines: [
|
|
186
|
+
'There is no conversation with that id in the history store.',
|
|
187
|
+
`Run ${list} to see the ones that can be resumed.`,
|
|
188
|
+
],
|
|
189
|
+
tone: 'warn',
|
|
190
|
+
};
|
|
191
|
+
case 'not-resumable': {
|
|
192
|
+
const why = refusal.reason === 'single-shot'
|
|
193
|
+
? `It was recorded by \`gth ${refusal.command ?? 'ask'}\`, a single-shot run, which keeps ` +
|
|
194
|
+
'no conversation state to pick up.'
|
|
195
|
+
: refusal.reason === 'no-thread'
|
|
196
|
+
? 'Its conversation state is not on record: either a checkpoint write failed while it ' +
|
|
197
|
+
'was running and it was marked unresumable, or it was recorded before conversation ' +
|
|
198
|
+
'state was kept.'
|
|
199
|
+
: refusal.reason === 'no-checkpoint'
|
|
200
|
+
? // GS2-107 — two ways to arrive here, and the sentence has to be true of both: the
|
|
201
|
+
// session ended before it wrote anything, or `gth history prune` removed the state
|
|
202
|
+
// afterwards. Nothing on the row distinguishes them, so neither is claimed.
|
|
203
|
+
'Its conversation state is not in the store — either the session ended before its ' +
|
|
204
|
+
'first turn completed, or `gth history prune` has since removed it.'
|
|
205
|
+
: 'Its conversation state could not be read from the store.';
|
|
206
|
+
return {
|
|
207
|
+
title: `Conversation #${refusal.id} cannot be resumed`,
|
|
208
|
+
lines: [why, `You can still read it with \`gth history show ${refusal.id}\`.`],
|
|
209
|
+
tone: 'warn',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case 'workspace-mismatch':
|
|
213
|
+
return {
|
|
214
|
+
title: `Conversation #${refusal.id} belongs to another project`,
|
|
215
|
+
lines: [
|
|
216
|
+
`It was recorded in ${refusal.stored}, and this session is in ${refusal.current}.`,
|
|
217
|
+
'A conversation is resumed from the directory it was recorded in, because its tools and ' +
|
|
218
|
+
'file paths point there. Change to that directory and run it again.',
|
|
219
|
+
'Nothing was changed.',
|
|
220
|
+
],
|
|
221
|
+
tone: 'warn',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** A bare `/resume`: the conversations this session could move to, and how. */
|
|
226
|
+
export function resumePickerNotice(candidates) {
|
|
227
|
+
if (candidates.length === 0) {
|
|
228
|
+
return {
|
|
229
|
+
title: 'No other conversation can be resumed',
|
|
230
|
+
lines: [
|
|
231
|
+
'Only a conversation recorded by an interactive `chat` or `code` session that completed ' +
|
|
232
|
+
'at least one turn can be resumed, and this session is not offered to itself.',
|
|
233
|
+
'Nothing was changed.',
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
title: 'Conversations you can resume',
|
|
239
|
+
lines: [
|
|
240
|
+
...formatConversationList(candidates),
|
|
241
|
+
'Resume one with /resume <id>. Later, from a shell, `gth history resume <id>` does the same.',
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** `/resume <id>` naming the conversation the session is already in: nothing to do, and said so. */
|
|
246
|
+
export function resumeSameConversationNotice(id) {
|
|
247
|
+
return {
|
|
248
|
+
title: `Already in conversation #${id}`,
|
|
249
|
+
lines: ['This session is recording under that conversation now.', 'Nothing was changed.'],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/** `/resume` on a surface with no conversation store behind it (the fixture agent). */
|
|
253
|
+
export function resumeUnavailableNotice() {
|
|
254
|
+
return {
|
|
255
|
+
title: 'Resume unavailable',
|
|
256
|
+
lines: ['This session has no conversation store to resume from.', 'Nothing was changed.'],
|
|
257
|
+
tone: 'warn',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** A resume whose apply step threw. The session is left in the conversation it was in. */
|
|
261
|
+
export function resumeFailedNotice(reason) {
|
|
262
|
+
return {
|
|
263
|
+
title: 'Resume did not happen',
|
|
264
|
+
lines: [`The session was left where it was: ${reason}`],
|
|
265
|
+
tone: 'warn',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=sessionResume.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionResume.js","sourceRoot":"","sources":["../../src/modules/sessionResume.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* can append more entries via {@link createCommandRegistry} without this module changing.
|
|
15
15
|
*/
|
|
16
16
|
import type { ApprovalRefusal, ApprovalRefusalLift, ApprovalRung, McpAnnotationTrustChange, McpAnnotationTrustView, ResolvedApprovals, ToolAnnotationHint } from '@gaunt-sloth/core/config.js';
|
|
17
|
+
import type { TokenBudget } from '@gaunt-sloth/core/config.js';
|
|
18
|
+
import type { AutocompactStatus } from '@gaunt-sloth/core/core/compactionThreshold.js';
|
|
17
19
|
import type { ApprovalGrant } from '@gaunt-sloth/core/core/approvals/grants.js';
|
|
20
|
+
import type { ConversationCompaction } from '@gaunt-sloth/core/core/compaction.js';
|
|
18
21
|
/**
|
|
19
22
|
* TUI-C63 — one advertised key binding: the keys as the user's keyboard spells them, and what
|
|
20
23
|
* they do. The keys are a display string, not a parsed chord — this module never matches on it.
|
|
@@ -39,12 +42,40 @@ export interface KeyBindingGroup {
|
|
|
39
42
|
export interface SlashCommandContext {
|
|
40
43
|
mode: string;
|
|
41
44
|
modelDisplayName: string;
|
|
45
|
+
/**
|
|
46
|
+
* CFG-38 — the resolved provider (`config.modelProviderType`, the raw `llm.type` the loader
|
|
47
|
+
* stashes), so `/status` and `/model` can render the shared `model (provider)` spelling instead
|
|
48
|
+
* of a model id that is ambiguous on its own (the same id is served by `anthropic` and by
|
|
49
|
+
* `openrouter`, and an OpenAI-compatible endpoint answers to a name that says nothing about who
|
|
50
|
+
* is serving it).
|
|
51
|
+
*
|
|
52
|
+
* Optional and legitimately absent — a module config hands the loader an already-built
|
|
53
|
+
* `BaseChatModel`, so there is no provider string to stash. That case renders the bare model
|
|
54
|
+
* with nothing extra, which is what `modelProviderLabel` does with an absent provider.
|
|
55
|
+
*/
|
|
56
|
+
modelProviderType?: string;
|
|
42
57
|
/** Count of committed turns so far (for `/help`-style introspection if needed). */
|
|
43
58
|
turnCount: number;
|
|
59
|
+
/**
|
|
60
|
+
* GS2-20 — the conversation this session is recording under, as `gth history list` numbers it,
|
|
61
|
+
* so `/status` can name what `gth history resume <id>` would take. Undefined when nothing is
|
|
62
|
+
* being recorded (history off, the store did not open, or a surface with no store at all).
|
|
63
|
+
*/
|
|
64
|
+
conversationId?: number;
|
|
44
65
|
/** Whether tool-call panels currently show their full args/result (drives `/verbose` copy). */
|
|
45
66
|
toolsExpanded: boolean;
|
|
46
67
|
/** Whether the docked debug panel is currently shown (drives `/debug` copy). */
|
|
47
68
|
debugVisible: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* EXT-161 — the preventive compaction threshold in force, for `/status`.
|
|
71
|
+
*
|
|
72
|
+
* A snapshot rather than a live read because `run` is synchronous and resolving a window can do
|
|
73
|
+
* I/O. The surface refreshes it when the session starts and again whenever `/autocompact`
|
|
74
|
+
* changes it, which is what makes `/status` report a session override rather than the seeded
|
|
75
|
+
* value it replaced. Omitted on a surface with no resolved model (the fixture agent), where
|
|
76
|
+
* `/status` simply says nothing about compaction instead of claiming a state it cannot know.
|
|
77
|
+
*/
|
|
78
|
+
autocompact?: AutocompactStatus;
|
|
48
79
|
/**
|
|
49
80
|
* TUI-C37 — whether terminal mouse reporting is currently on (drives `/mouse` copy). Undefined on
|
|
50
81
|
* surfaces that have no mouse layer at all, where `/mouse` reports itself unavailable rather than
|
|
@@ -146,6 +177,12 @@ export interface DebugDumpInput {
|
|
|
146
177
|
*/
|
|
147
178
|
export interface ConfigSummaryInput {
|
|
148
179
|
modelDisplayName?: string;
|
|
180
|
+
/**
|
|
181
|
+
* CFG-38 — `config.modelProviderType`, rendered beside the model. Both surfaces already hand
|
|
182
|
+
* this function the whole resolved config, which carries the field, so widening the structural
|
|
183
|
+
* type is all the threading `/config` needs.
|
|
184
|
+
*/
|
|
185
|
+
modelProviderType?: string;
|
|
149
186
|
agent?: {
|
|
150
187
|
backend?: string;
|
|
151
188
|
};
|
|
@@ -267,6 +304,43 @@ export interface SlashCommandResult {
|
|
|
267
304
|
reasoning: string;
|
|
268
305
|
turnNumber: number;
|
|
269
306
|
};
|
|
307
|
+
/**
|
|
308
|
+
* GS2-23 — a request from `/compact` to fold the older conversation into a summary in the
|
|
309
|
+
* model's context. Takes the `approvals: { rung }` route for the same reason: the command cannot
|
|
310
|
+
* reach the runner, so it states the request and the surface awaits
|
|
311
|
+
* `runner.compactConversation` and commits {@link compactionNotice} for what LANDED. `focus` is
|
|
312
|
+
* the free text after the command — what the summary should concentrate on.
|
|
313
|
+
*/
|
|
314
|
+
compact?: {
|
|
315
|
+
focus?: string;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* EXT-161 — a request from `/autocompact` to report or move the preventive compaction threshold.
|
|
319
|
+
*
|
|
320
|
+
* Takes the same route as `approvals` and `compact`: the command cannot reach the agent, so it
|
|
321
|
+
* states the request — already PARSED, so the surface never re-reads the grammar — and the
|
|
322
|
+
* surface applies it and reports what landed. `{ show: true }` is the bare form, which reports
|
|
323
|
+
* rather than erroring; `{ budget }` sets it for the rest of the session.
|
|
324
|
+
*
|
|
325
|
+
* A malformed argument never reaches here: the command answers with its own `notice` and leaves
|
|
326
|
+
* the threshold in force, because a typo must not switch a protection off.
|
|
327
|
+
*/
|
|
328
|
+
autocompact?: {
|
|
329
|
+
show: true;
|
|
330
|
+
} | {
|
|
331
|
+
budget: TokenBudget;
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* GS2-20 — a request from `/resume` to re-enter a stored conversation. With an `id` the surface
|
|
335
|
+
* resolves and applies it through the one seam in `sessionResume.ts` — the same two calls
|
|
336
|
+
* `--resume <id>` makes at boot — and commits the resumed-conversation banner and the restored
|
|
337
|
+
* turns, or the refusal. With no `id` the surface lists the conversations that can be resumed,
|
|
338
|
+
* leaving out the one it is in. The command itself stays pure: it cannot reach the store or the
|
|
339
|
+
* runner, so it states the request.
|
|
340
|
+
*/
|
|
341
|
+
resume?: {
|
|
342
|
+
id?: number;
|
|
343
|
+
};
|
|
270
344
|
/** When true, the component quits the app (runs `onExit`). */
|
|
271
345
|
exit?: boolean;
|
|
272
346
|
}
|
|
@@ -617,6 +691,74 @@ export declare function resolveDebugDumpRedact(resolvedConfig: unknown, args: st
|
|
|
617
691
|
* best-effort pattern-based, so even the softened note still says review-before-sharing.
|
|
618
692
|
*/
|
|
619
693
|
export declare function debugDumpNotice(archiveDir: string, redacted: boolean): SlashCommandNotice;
|
|
694
|
+
/**
|
|
695
|
+
* TUI-C56 — the same archive path, written for a screen the session is no longer on.
|
|
696
|
+
*
|
|
697
|
+
* The notice above is painted into the session's frame, which on the full-screen surface lives in
|
|
698
|
+
* the alternate screen and is discarded the moment the user exits. This is the block deferred to
|
|
699
|
+
* the exit-output channel instead, so the path is still on the terminal after the session is gone.
|
|
700
|
+
*
|
|
701
|
+
* It repeats rather than replaces the notice, and it is one self-contained line rather than the
|
|
702
|
+
* notice's seven, because the two are read in different places: the notice arrives in a session
|
|
703
|
+
* the user is still in and can act on immediately, while this lands under whatever the user's
|
|
704
|
+
* terminal held before they started. So it carries its own label and its own statement of what
|
|
705
|
+
* redaction did — a reader who scrolled past the session start has no other context — and stops
|
|
706
|
+
* there: the opt-out instructions and the review-before-sharing detail belong to the moment of
|
|
707
|
+
* running the command, not to a line found later.
|
|
708
|
+
*/
|
|
709
|
+
export declare function debugDumpExitLine(archiveDir: string, redacted: boolean): string;
|
|
710
|
+
/**
|
|
711
|
+
* GS2-23 — the line a surface prints the moment `/compact` starts, because the model call behind
|
|
712
|
+
* it takes seconds and a command that goes quiet reads as a command that did nothing (DL-1).
|
|
713
|
+
*/
|
|
714
|
+
export declare const COMPACTING_LINE = "Compacting the conversation \u2014 the model is summarising the older messages\u2026";
|
|
715
|
+
/**
|
|
716
|
+
* GS2-23 — the notice for a landed `/compact`, built from what the runner RETURNS rather than from
|
|
717
|
+
* what was asked for, so it can only describe the compaction actually applied.
|
|
718
|
+
*
|
|
719
|
+
* It says three things, because those are the three the person cannot otherwise observe: what was
|
|
720
|
+
* folded, what was kept, and how much smaller the model's context is. And it says what did NOT
|
|
721
|
+
* change — the transcript on screen — because the screen is the person's record and a notice that
|
|
722
|
+
* only mentioned the model would leave them expecting lines to vanish. The size is a message count
|
|
723
|
+
* and a character estimate; tokens are not counted here.
|
|
724
|
+
*/
|
|
725
|
+
export declare function compactionNotice(outcome: ConversationCompaction, focus?: string): SlashCommandNotice;
|
|
726
|
+
/** GS2-23 — `/compact` on a surface with no conversation state behind it (the fixture agent). */
|
|
727
|
+
export declare function compactionUnavailableNotice(): SlashCommandNotice;
|
|
728
|
+
/**
|
|
729
|
+
* GS2-23 — `/compact` whose summary call failed, or that was refused (a turn still running). The
|
|
730
|
+
* conversation is left as it was in every such case, and the notice says so before it says why.
|
|
731
|
+
*/
|
|
732
|
+
export declare function compactionFailedNotice(reason: string): SlashCommandNotice;
|
|
733
|
+
/**
|
|
734
|
+
* EXT-161 — how a resolved threshold and its provenance read to a person.
|
|
735
|
+
*
|
|
736
|
+
* One renderer for both `/autocompact` and `/status`, so the two cannot describe the same number
|
|
737
|
+
* differently — which they would within a release of being written separately.
|
|
738
|
+
*/
|
|
739
|
+
export declare function autocompactLines(status: AutocompactStatus): string[];
|
|
740
|
+
/**
|
|
741
|
+
* EXT-161 — the notice `/autocompact` prints, whether it reported or changed the threshold.
|
|
742
|
+
*
|
|
743
|
+
* A request to CHANGE the threshold while the config has compaction off is refused by the
|
|
744
|
+
* controller, and the status that comes back still says `enabled: false`. That combination is
|
|
745
|
+
* reported as the refusal it is: a notice titled "threshold set" over a body saying nothing will
|
|
746
|
+
* ever fire would tell the user the opposite of what happened. The bare report keeps the OFF lines.
|
|
747
|
+
*/
|
|
748
|
+
export declare function autocompactNotice(status: AutocompactStatus, changed: boolean): SlashCommandNotice;
|
|
749
|
+
/**
|
|
750
|
+
* EXT-161 — `/autocompact` on a surface with no resolved model behind it (the fixture agent), or
|
|
751
|
+
* before the agent has initialised. It reports unavailability rather than inventing a number.
|
|
752
|
+
*/
|
|
753
|
+
export declare function autocompactUnavailableNotice(): SlashCommandNotice;
|
|
754
|
+
/**
|
|
755
|
+
* EXT-161 — a malformed `/autocompact` argument.
|
|
756
|
+
*
|
|
757
|
+
* **The previous threshold stays in force.** A bad argument must not disable compaction: the user
|
|
758
|
+
* asked to change a number, not to switch a protection off, and a typo that silently turned the
|
|
759
|
+
* feature off would be discovered only by an overflow much later.
|
|
760
|
+
*/
|
|
761
|
+
export declare function autocompactRejectedNotice(message: string): SlashCommandNotice;
|
|
620
762
|
/**
|
|
621
763
|
* Build the default command registry. Returns a fresh array each call so callers may push
|
|
622
764
|
* extension commands onto it (EXT-5) without sharing mutable module state.
|