@itookit/dsht 0.3.7 → 0.5.1

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.
Files changed (132) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +31 -11
  3. package/README.zh.md +31 -11
  4. package/dist/cli/dsht.js +207 -19
  5. package/dist/cli/startup.d.ts +40 -0
  6. package/dist/cli/startup.js +295 -0
  7. package/dist/cli/trace-summary.d.ts +78 -0
  8. package/dist/cli/trace-summary.js +241 -0
  9. package/dist/cli/verifier.d.ts +60 -0
  10. package/dist/cli/verifier.js +242 -0
  11. package/dist/contracts.d.ts +344 -0
  12. package/dist/contracts.js +1 -0
  13. package/dist/controller/commands.d.ts +47 -0
  14. package/dist/controller/commands.js +322 -0
  15. package/dist/controller/connection.d.ts +11 -29
  16. package/dist/controller/connection.js +26 -60
  17. package/dist/controller/controller.d.ts +619 -164
  18. package/dist/controller/controller.js +1420 -141
  19. package/dist/controller/index.d.ts +8 -1
  20. package/dist/controller/index.js +5 -0
  21. package/dist/controller/loop-contract.d.ts +136 -0
  22. package/dist/controller/loop-contract.js +308 -0
  23. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  24. package/dist/controller/loop-prompts-schema.js +144 -0
  25. package/dist/controller/loop-prompts.d.ts +55 -0
  26. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  27. package/dist/controller/loop-prompts.generated.js +185 -0
  28. package/dist/controller/loop-prompts.js +104 -0
  29. package/dist/controller/loop-protocols.d.ts +39 -0
  30. package/dist/controller/loop-protocols.js +115 -0
  31. package/dist/controller/loop.d.ts +275 -0
  32. package/dist/controller/loop.js +378 -0
  33. package/dist/controller/prompts.d.ts +54 -0
  34. package/dist/controller/prompts.js +162 -0
  35. package/dist/controller/trace-log.d.ts +45 -0
  36. package/dist/controller/trace-log.js +144 -0
  37. package/dist/controller/verifier.d.ts +126 -0
  38. package/dist/controller/verifier.js +75 -0
  39. package/dist/cost/index.d.ts +1 -1
  40. package/dist/cost/index.js +1 -1
  41. package/dist/cost/ledger.d.ts +0 -1
  42. package/dist/cost/ledger.js +0 -1
  43. package/dist/json.d.ts +18 -0
  44. package/dist/json.js +19 -0
  45. package/dist/references.d.ts +25 -0
  46. package/dist/references.js +26 -0
  47. package/dist/session/connection-view.d.ts +2 -11
  48. package/dist/session/controller.d.ts +82 -72
  49. package/dist/session/controller.js +211 -209
  50. package/dist/session/history.d.ts +9 -1
  51. package/dist/session/history.js +1 -9
  52. package/dist/session/index.d.ts +9 -4
  53. package/dist/session/index.js +7 -3
  54. package/dist/session/info.d.ts +25 -52
  55. package/dist/session/info.js +39 -25
  56. package/dist/session/markdown.js +1 -1
  57. package/dist/session/math.js +1 -1
  58. package/dist/session/mutation-gate.d.ts +51 -0
  59. package/dist/session/mutation-gate.js +73 -0
  60. package/dist/session/navigation.d.ts +2 -89
  61. package/dist/session/navigation.js +2 -129
  62. package/dist/session/peek.d.ts +38 -0
  63. package/dist/session/peek.js +103 -0
  64. package/dist/session/references.d.ts +2 -20
  65. package/dist/session/references.js +1 -26
  66. package/dist/session/runtime.d.ts +26 -0
  67. package/dist/session/runtime.js +28 -0
  68. package/dist/session/telemetry.d.ts +12 -13
  69. package/dist/session/telemetry.js +27 -58
  70. package/dist/session/transcript.d.ts +0 -6
  71. package/dist/session/transcript.js +2 -15
  72. package/dist/session/types.d.ts +25 -0
  73. package/dist/session/types.js +0 -1
  74. package/dist/session-title.d.ts +9 -0
  75. package/dist/session-title.js +21 -0
  76. package/dist/shell/controller.d.ts +97 -0
  77. package/dist/shell/controller.js +158 -0
  78. package/dist/shell/index.d.ts +5 -0
  79. package/dist/shell/index.js +3 -0
  80. package/dist/shell/runner.d.ts +38 -0
  81. package/dist/shell/runner.js +147 -0
  82. package/dist/slash/index.d.ts +10 -0
  83. package/dist/slash/index.js +7 -0
  84. package/dist/slash/parse.d.ts +166 -0
  85. package/dist/slash/parse.js +259 -0
  86. package/dist/slash/pipeline.d.ts +140 -0
  87. package/dist/slash/pipeline.js +115 -0
  88. package/dist/slash/registry.d.ts +88 -0
  89. package/dist/slash/registry.js +177 -0
  90. package/dist/state.d.ts +14 -4
  91. package/dist/state.js +3 -2
  92. package/dist/text.d.ts +28 -0
  93. package/dist/text.js +55 -0
  94. package/dist/transport/events.d.ts +104 -0
  95. package/dist/transport/events.js +149 -0
  96. package/dist/transport/wire.d.ts +9 -17
  97. package/dist/transport/wire.js +2 -27
  98. package/dist/ui/app.js +865 -431
  99. package/dist/ui/chat/header.js +1 -1
  100. package/dist/ui/chat/history-view.d.ts +1 -1
  101. package/dist/ui/chat/history-view.js +1 -1
  102. package/dist/ui/chat/loop-status.d.ts +11 -0
  103. package/dist/ui/chat/loop-status.js +28 -0
  104. package/dist/ui/chat/navigation-model.d.ts +86 -0
  105. package/dist/ui/chat/navigation-model.js +107 -0
  106. package/dist/ui/chat/shell-view.d.ts +47 -0
  107. package/dist/ui/chat/shell-view.js +145 -0
  108. package/dist/ui/chat/status.d.ts +47 -3
  109. package/dist/ui/chat/status.js +65 -50
  110. package/dist/ui/chat/viewport.d.ts +1 -1
  111. package/dist/ui/dialogs/cost.d.ts +21 -4
  112. package/dist/ui/dialogs/cost.js +7 -12
  113. package/dist/ui/dialogs/index.d.ts +22 -5
  114. package/dist/ui/dialogs/index.js +19 -3
  115. package/dist/ui/dialogs/loop.d.ts +43 -0
  116. package/dist/ui/dialogs/loop.js +224 -0
  117. package/dist/ui/dialogs/peek.d.ts +25 -0
  118. package/dist/ui/dialogs/peek.js +35 -0
  119. package/dist/ui/dialogs/picker.d.ts +2 -0
  120. package/dist/ui/dialogs/picker.js +4 -2
  121. package/dist/ui/input/mouse.d.ts +12 -2
  122. package/dist/ui/input/mouse.js +20 -7
  123. package/dist/ui/input/references.d.ts +1 -1
  124. package/dist/ui/status/model.d.ts +7 -0
  125. package/dist/ui/status/model.js +5 -0
  126. package/dist/ui/theme/index.d.ts +6 -1
  127. package/dist/ui/theme/index.js +2 -1
  128. package/package.json +6 -4
  129. package/dist/ui/commands/parse.d.ts +0 -99
  130. package/dist/ui/commands/parse.js +0 -126
  131. package/dist/ui/commands/registry.d.ts +0 -33
  132. package/dist/ui/commands/registry.js +0 -73
@@ -0,0 +1,378 @@
1
+ /** Longest evidence line and finding kept from a verdict, so untrusted model output stays bounded. */
2
+ const MAX_EVIDENCE_CHARS = 600;
3
+ const MAX_FINDING_CHARS = 300;
4
+ const MAX_FINDINGS = 8;
5
+ /** Longest reason a verifier may give for stopping early. */
6
+ const MAX_REASON_CHARS = 300;
7
+ /** Consecutive attempts on one step that may fail to improve the score before the run stops.
8
+ *
9
+ * One plateau can be verifier jitter, so it is tolerated; two in a row mean the retry is not
10
+ * converging. Set to 1 to stop on the first non-improvement (which includes a strict decrease).
11
+ */
12
+ const STALL_STREAK = 2;
13
+ /** Apply a protocol's defaults and reject a range that cannot run.
14
+ * @param protocol - Protocol being started.
15
+ * @param options - Flags exactly as parsed, absent when the operator omitted them.
16
+ * @returns The resolved limits, or undefined when `to < from`.
17
+ */
18
+ export function resolveLoop(protocol, options) {
19
+ const from = options.from ?? 1;
20
+ const to = options.to ?? protocol.steps;
21
+ const score = options.score ?? protocol.defaultScore ?? 8;
22
+ const tries = options.tries ?? protocol.defaultTries ?? 10;
23
+ if (to < from)
24
+ return undefined;
25
+ return { from, to, score, tries };
26
+ }
27
+ /** Whether a run's rounds cover the whole record rather than a selected range.
28
+ *
29
+ * Only this case may report more than "the rounds that ran passed": the last round is then the
30
+ * record's consolidation round, and the verifier is handed every earlier round to re-check.
31
+ * @param from - First step of the run.
32
+ * @param to - Last step of the run.
33
+ * @param steps - Steps the protocol defines.
34
+ * @returns True when the run starts at the first round and ends at the last one.
35
+ */
36
+ export function coversWholeProtocol(from, to, steps) {
37
+ return from === 1 && to === steps;
38
+ }
39
+ /** How to describe the rounds a finished run covered.
40
+ *
41
+ * `passed` only ever claims the rounds that ran, so the snapshot carries this phrase and every place
42
+ * that reports a pass says it too: a pass over the whole record and a pass over a selected range
43
+ * must not read the same.
44
+ * @param progress - Run limits and the protocol's total round count.
45
+ * @returns One phrase, e.g. `rounds 1–10/10` or `rounds 1–3/10 · selected range`.
46
+ */
47
+ function loopScope(progress) {
48
+ const { from, to, total } = progress;
49
+ const label = from === to ? `round ${from}/${total}` : `rounds ${from}–${to}/${total}`;
50
+ return coversWholeProtocol(from, to, total) ? label : `${label} · selected range`;
51
+ }
52
+ /** Read the result out of the last block of one reply.
53
+ *
54
+ * The block must be in the assistant's text, not reasoning, and this takes the last one so a reply
55
+ * that quotes its verifier still reports its own verdict. A missing, malformed or foreign block is
56
+ * undefined; a valid block with no usable field is an empty result, which counts as a failed attempt.
57
+ * @param text - One turn's assistant text.
58
+ * @param protocol - Protocol whose marker and kind identify the block.
59
+ * @returns The reported score and verdict, or undefined when the reply carries no valid block.
60
+ */
61
+ export function parseLoopResult(text, protocol) {
62
+ let body;
63
+ const segments = text.split('```');
64
+ // Fenced blocks are the odd-indexed segments: prose, block, prose, block, …
65
+ for (let index = 1; index < segments.length; index += 2) {
66
+ const segment = segments[index];
67
+ const newline = segment.indexOf('\n');
68
+ if ((newline === -1 ? segment : segment.slice(0, newline)).trim() !== protocol.marker)
69
+ continue;
70
+ body = newline === -1 ? '' : segment.slice(newline + 1);
71
+ }
72
+ if (body === undefined)
73
+ return undefined;
74
+ try {
75
+ const parsed = JSON.parse(body.trim());
76
+ if (parsed.kind !== protocol.kind)
77
+ return undefined;
78
+ return readResultFields(parsed);
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ /** Validate the score and verdict of one decoded result object.
85
+ *
86
+ * Shared with the forked verifier's verdict file, so a score means the same thing however it
87
+ * travelled: an out-of-range or unknown field is absent rather than a value the loop would trust.
88
+ * @param parsed - Decoded object expected to carry `score` and `status`.
89
+ * @returns The usable fields; an empty result when the object carried none, and `undefined` when it
90
+ * claimed an early stop that breaks the rules — such a claim is not a verdict at all, so the
91
+ * caller reports verification as unusable instead of guessing what was meant.
92
+ */
93
+ export function readResultFields(parsed) {
94
+ const fields = parsed;
95
+ const score = typeof fields.score === 'number' ? fields.score : Number(fields.score);
96
+ const status = fields.status === 'done' || fields.status === 'retry' || fields.status === 'blocked' || fields.status === 'abstained'
97
+ ? fields.status : undefined;
98
+ const rawFindings = fields.top_findings;
99
+ const findings = Array.isArray(rawFindings)
100
+ ? rawFindings.filter((item) => typeof item === 'string' && item.trim() !== '')
101
+ .slice(0, MAX_FINDINGS).map(item => item.slice(0, MAX_FINDING_CHARS))
102
+ : [];
103
+ const text = (value, limit) => typeof value === 'string' && value.trim() !== '' ? value.slice(0, limit) : undefined;
104
+ const evidence = text(fields.evidence, MAX_EVIDENCE_CHARS);
105
+ const reason = text(fields.reason, MAX_REASON_CHARS);
106
+ const needs = text(fields.needs, MAX_REASON_CHARS);
107
+ // An advisory line that never changes control: "nothing to change here" is an explanation, not a
108
+ // shortcut past the rounds that have not run yet.
109
+ const explanation = text(fields.explanation, MAX_EVIDENCE_CHARS);
110
+ const hasScore = Number.isFinite(score) && score >= 0 && score <= 10;
111
+ // `blocked` is a fact about the task, so it is the one field that can override the score; the
112
+ // model's own `status` is kept only as an explanation, never as the run's state.
113
+ const blocked = fields.blocked === true || status === 'blocked';
114
+ const abstained = status === 'abstained';
115
+ const base = {
116
+ ...(hasScore ? { score } : {}),
117
+ ...(status === undefined ? {} : { status }),
118
+ ...(explanation === undefined ? {} : { explanation }),
119
+ ...(findings.length === 0 ? {} : { findings }),
120
+ ...(evidence === undefined ? {} : { evidence }),
121
+ };
122
+ // An early stop is a claim about the task, so it is validated as a whole and never partially
123
+ // honored: exactly one of "proved impossible" and "cannot judge", a reason a reader can act on,
124
+ // no score for `abstained` to hide behind, and an `exit_reason` label that agrees with the flags.
125
+ const claims = blocked || abstained || fields.exit_reason !== undefined;
126
+ if (!claims)
127
+ return base;
128
+ if (reason === undefined || blocked === abstained || hasScore)
129
+ return undefined;
130
+ const exitReason = abstained ? 'needs-human' : 'cannot-fix';
131
+ if (fields.exit_reason !== undefined && fields.exit_reason !== exitReason)
132
+ return undefined;
133
+ return { ...base, ...(blocked ? { blocked: true } : {}), ...(abstained ? { abstained: true } : {}),
134
+ exitReason, reason, ...(abstained && needs !== undefined ? { needs } : {}) };
135
+ }
136
+ /** Assistant text of the turn that just finished: from the last user/context row to the end.
137
+ * @param messages - Projected conversation messages.
138
+ * @returns The assistant text, empty when the turn produced none.
139
+ */
140
+ export function latestAssistantText(messages) {
141
+ const parts = [];
142
+ for (let index = messages.length - 1; index >= 0; index--) {
143
+ const message = messages[index];
144
+ if (message.role === 'You' || message.role === 'Context')
145
+ break;
146
+ if (message.role === 'Assistant')
147
+ parts.unshift(message.text);
148
+ }
149
+ return parts.join('\n');
150
+ }
151
+ /** One run of the loop: which step and attempt is in flight, and what happens next.
152
+ *
153
+ * No I/O: the controller sends the prompt this returns and feeds back the parsed score, which keeps
154
+ * the state machine trivially testable and identical for every protocol.
155
+ */
156
+ export class ScoredLoop {
157
+ runId;
158
+ sessionId;
159
+ protocol;
160
+ limits;
161
+ step;
162
+ attempt = 1;
163
+ best = 0;
164
+ noProgress = 0;
165
+ phase = 'running';
166
+ awaiting = false;
167
+ /** What the live run is waiting on; only the controller's own sends and verdicts set it. */
168
+ activity;
169
+ /** Why the run stopped; set once, together with a terminal phase. */
170
+ terminalReason;
171
+ noteText;
172
+ interaction;
173
+ exit;
174
+ /** When this run began, so a reader can clock it even while no host turn is running. */
175
+ startedAt = Date.now();
176
+ constructor(runId, sessionId, protocol, limits) {
177
+ this.runId = runId;
178
+ this.sessionId = sessionId;
179
+ this.protocol = protocol;
180
+ this.limits = limits;
181
+ this.step = limits.from;
182
+ }
183
+ /** Snapshot the UI renders; the loop keeps the authoritative numbers. */
184
+ get progress() {
185
+ const stepLabel = this.protocol.stepLabel?.(this.step);
186
+ return { runId: this.runId, title: this.protocol.title, startedAt: this.startedAt, ...this.limits, total: this.protocol.steps, scope: loopScope({ ...this.limits, total: this.protocol.steps }),
187
+ step: this.step, attempt: this.attempt, best: this.best, phase: this.phase, active: this.active,
188
+ // Activity describes a run that is working: a paused run waits for a person, not for the host.
189
+ ...(this.phase !== 'running' || this.activity === undefined ? {} : { activity: this.activity }),
190
+ ...(this.terminalReason === undefined ? {} : { terminalReason: this.terminalReason }),
191
+ ...(stepLabel === undefined ? {} : { stepLabel }),
192
+ ...(this.noteText === undefined ? {} : { note: this.noteText }),
193
+ ...(this.interaction === undefined ? {} : { interaction: this.interaction }),
194
+ ...(this.exit === undefined ? {} : { exit: this.exit }) };
195
+ }
196
+ /** Attach one line about the attempt just decided, shown until the next verdict replaces it.
197
+ * @param text - Note to show, or an empty string to clear it.
198
+ */
199
+ note(text) { this.noteText = text === '' ? undefined : text; }
200
+ /** Whether the run still owns its session: it may send, settle — or be answered.
201
+ *
202
+ * A paused run counts: it is not finished, so a new run must not silently replace it (§8.3.1). What
203
+ * it is *not* doing is working; `activity` is absent while it waits.
204
+ */
205
+ get active() {
206
+ return this.phase === 'running' || (this.phase === 'needs-human' && this.terminalReason === undefined);
207
+ }
208
+ /** Whether this run has sent any prompt yet, so `intro` knows the opening one is still owed. */
209
+ briefed = false;
210
+ /** Whether a prompt was sent and its reply is still outstanding. */
211
+ get settled() { return this.awaiting; }
212
+ /** The opening prompt; the caller sends it and then calls `sent()`. */
213
+ start() { return this.protocol.brief(this.limits, this.step, this.attempt); }
214
+ /** The opening prompt, when this run has not sent one yet; undefined once it has.
215
+ *
216
+ * A record that verifies first never sends its brief at the start — it checks the artifact instead —
217
+ * so the first work message after a failed check has to be the brief. A follow-up would open with
218
+ * "read the results and opinions above" and refer to a previous version, and neither exists on the
219
+ * first work turn; the round's own checklist only reaches the agent through this prompt.
220
+ */
221
+ intro() {
222
+ return this.briefed ? undefined : this.protocol.brief(this.limits, this.step, this.attempt);
223
+ }
224
+ /** The prompt that asks for work on the step in flight: the owed brief, else the follow-up. */
225
+ workPrompt() {
226
+ return this.intro() ?? this.protocol.followUp(this.limits, this.step, this.attempt);
227
+ }
228
+ /** Record that the outstanding prompt reached the host. */
229
+ sent() { this.awaiting = true; this.activity = 'turn'; this.briefed = true; }
230
+ /** Record that this attempt is judged by a forked verifier instead of the session's own turn. */
231
+ verifying() { this.awaiting = true; this.activity = 'verify'; }
232
+ /** Record that the turn ended and its result is being read, which is neither work nor a verdict. */
233
+ settling() { this.activity = 'settle'; }
234
+ /** Consume one finished attempt.
235
+ * @param result - Parsed result, or undefined when the reply carried no usable block.
236
+ * @returns What the loop does next.
237
+ */
238
+ settle(result) {
239
+ this.awaiting = false;
240
+ this.activity = 'settle';
241
+ // A verifier that cannot judge asks for a person. The run pauses rather than ending: the operator
242
+ // can answer it (`/loop answer`) or end it (`/loop abort`), and no attempt is consumed either way.
243
+ if (result?.abstained === true) {
244
+ this.pause({ kind: 'verdict', text: result.reason ?? 'the verifier needs a person',
245
+ ...(result.needs === undefined ? {} : { needs: result.needs }) });
246
+ return { kind: 'needs-human' };
247
+ }
248
+ // A verifier that proved the task impossible ends the run instead of burning the budget.
249
+ // Either spelling ends the run: `blocked` is the fact, `status` the legacy way of saying it.
250
+ if (result?.blocked === true || result?.status === 'blocked') {
251
+ this.phase = 'blocked';
252
+ this.terminalReason = 'blocked';
253
+ if (result.reason !== undefined)
254
+ this.exit = { reason: result.reason };
255
+ return { kind: 'blocked' };
256
+ }
257
+ const score = result?.score;
258
+ const previousBest = this.best;
259
+ if (score !== undefined) {
260
+ if (score > previousBest)
261
+ this.noProgress = 0;
262
+ else if (this.attempt > 1)
263
+ this.noProgress += 1;
264
+ this.best = Math.max(this.best, score);
265
+ }
266
+ if (score !== undefined && score >= this.limits.score) {
267
+ if (this.step >= this.limits.to) {
268
+ this.phase = 'passed';
269
+ this.terminalReason = 'pass';
270
+ return { kind: 'passed' };
271
+ }
272
+ this.step += 1;
273
+ this.attempt = 1;
274
+ this.best = 0;
275
+ this.noProgress = 0;
276
+ return { kind: 'continue', prompt: this.workPrompt() };
277
+ }
278
+ // Retries that keep not improving are not converging: spending the rest of the step's budget on
279
+ // them only reaches the same conclusion later, so the run stops and says so.
280
+ if (this.noProgress >= STALL_STREAK) {
281
+ this.phase = 'stalled';
282
+ this.terminalReason = 'stalled';
283
+ return { kind: 'stalled' };
284
+ }
285
+ // A missing or low score is a failed attempt and costs one from the step's budget.
286
+ this.attempt += 1;
287
+ if (this.attempt > this.limits.tries) {
288
+ this.phase = 'exhausted';
289
+ this.terminalReason = 'exhausted';
290
+ return { kind: 'exhausted' };
291
+ }
292
+ return { kind: 'continue', prompt: this.workPrompt() };
293
+ }
294
+ /** Stop the run; a cancelled run never sends again.
295
+ * @param reason - Who ended it, so a cancellation is not confused with a verdict.
296
+ */
297
+ cancel(reason = 'user-cancelled') {
298
+ this.phase = 'cancelled';
299
+ this.awaiting = false;
300
+ this.activity = undefined;
301
+ this.interaction = undefined;
302
+ this.terminalReason = reason;
303
+ }
304
+ /** End the run because independent verification was impossible.
305
+ *
306
+ * Deliberately not a verdict: no attempt is consumed, so the run stops on an infrastructure
307
+ * failure instead of pretending the reviewer produced nothing.
308
+ * @param reason - Which verification failure ended it.
309
+ */
310
+ unavailable(reason = 'verifier-unavailable') {
311
+ this.phase = 'unavailable';
312
+ this.awaiting = false;
313
+ this.activity = undefined;
314
+ this.interaction = undefined;
315
+ this.terminalReason = reason;
316
+ }
317
+ /** End the run because the whole-run deadline expired.
318
+ *
319
+ * A budget stop, not a verdict: it bounds the sum of all steps, attempts and verifier retries, so
320
+ * no attempt is consumed and `best` is untouched.
321
+ */
322
+ deadline() {
323
+ this.phase = 'deadline';
324
+ this.awaiting = false;
325
+ this.activity = undefined;
326
+ this.interaction = undefined;
327
+ this.terminalReason = 'deadline';
328
+ }
329
+ /** Pause the run because a judgment asked for a person.
330
+ *
331
+ * Deliberately not terminal: no `terminalReason` is written, so the run still owns its session and
332
+ * the progress line can offer `/loop answer` or `/loop abort` instead of a summary.
333
+ * @param request - What is being asked, and what the answer has to supply.
334
+ */
335
+ pause(request) {
336
+ this.phase = 'needs-human';
337
+ this.awaiting = false;
338
+ this.activity = undefined;
339
+ this.interaction = request;
340
+ }
341
+ /** Resume a paused run after the operator supplied what the judgment was missing.
342
+ *
343
+ * No attempt is consumed: the answer only adds a condition, and the current artifact is judged again
344
+ * under a new verification identity. The attempt counts as outstanding, so that verdict may decide it.
345
+ */
346
+ resume(activity = 'verify') {
347
+ this.phase = 'running';
348
+ this.awaiting = true;
349
+ this.activity = activity;
350
+ this.interaction = undefined;
351
+ this.noteText = undefined;
352
+ }
353
+ /** The prompt that asks again with the operator's addition.
354
+ *
355
+ * Used when no forked verifier judges this run: the answer becomes the next attempt's instruction
356
+ * instead of a condition for a judgment, and asking again costs no attempt by itself.
357
+ * @param answer - What the operator supplied.
358
+ * @returns The prompt to send.
359
+ */
360
+ answerPrompt(answer) {
361
+ return `${this.protocol.followUp(this.limits, this.step, this.attempt)}\n\n## 操作者的补充判断\n${answer}`;
362
+ }
363
+ /** End the run because the request cannot be answered from here.
364
+ *
365
+ * Used for the cases the operator cannot unblock through this client — a verifier child whose own
366
+ * session asked a question, or a prompt the host refused to accept — as opposed to `pause`, where
367
+ * `/loop answer` supplies what the judgment was missing. No attempt is consumed in either case.
368
+ * @param request - The approval or question the host is waiting for, or a verdict's request.
369
+ * @param reason - Which kind of human request this is.
370
+ */
371
+ human(request, reason = 'verifier-needs-human') {
372
+ this.phase = 'needs-human';
373
+ this.awaiting = false;
374
+ this.activity = undefined;
375
+ this.terminalReason = reason;
376
+ this.interaction = request;
377
+ }
378
+ }
@@ -0,0 +1,54 @@
1
+ import type { SavedPrompt } from '../contracts.ts';
2
+ /** Largest single saved prompt; a shortcut is meant to be reusable, not a document store. */
3
+ export declare const MAX_PROMPT_CHARS: number;
4
+ /** Most saved prompts one client keeps. */
5
+ export declare const MAX_SAVED_PROMPTS = 500;
6
+ /** Read and persist shortcut prompts; an absent path keeps them in memory for this run.
7
+ *
8
+ * Mutations are queued: a save, edit or delete computes and writes inside the queue, so two rapid
9
+ * commands can never interleave their writes and leave the file disagreeing with the list.
10
+ */
11
+ export declare class PromptStore {
12
+ readonly path?: string | undefined;
13
+ private items;
14
+ private loaded;
15
+ /** Tail of the mutation queue; the next operation starts only after the previous one settled. */
16
+ private queue;
17
+ /** Last read or write failure, shown by the saved-prompt picker. */
18
+ error: string | undefined;
19
+ constructor(path?: string | undefined);
20
+ /** Entries in the order they were saved, oldest first. */
21
+ get list(): readonly SavedPrompt[];
22
+ /** Read the file once; a missing file is simply an empty list.
23
+ *
24
+ * A malformed document leaves the list empty and is reported through `error` rather than thrown,
25
+ * because a broken shortcuts file must not stop the client from starting.
26
+ * @returns Whether the result differs from the empty default, so a caller knows to republish.
27
+ */
28
+ load(): Promise<boolean>;
29
+ /** Add one prompt; a text already saved is returned instead of duplicated.
30
+ * @param value - Prompt text exactly as the user typed it.
31
+ * @returns The saved entry.
32
+ */
33
+ save(value: string): Promise<SavedPrompt>;
34
+ /** Replace one saved prompt's text.
35
+ * @param id - Entry identity.
36
+ * @param value - Replacement text.
37
+ * @returns False when the entry no longer exists.
38
+ */
39
+ update(id: string, value: string): Promise<boolean>;
40
+ /** Drop one saved prompt.
41
+ * @param id - Entry identity.
42
+ * @returns False when it was already gone.
43
+ */
44
+ remove(id: string): Promise<boolean>;
45
+ /** Queue one operation behind everything already queued, whether or not the last one failed.
46
+ * @param task - Operation to run when it reaches the head of the queue.
47
+ * @returns The operation's own result or failure.
48
+ */
49
+ private enqueue;
50
+ /** Apply a new list, restoring the previous one when the write fails, so memory follows the file. */
51
+ private commit;
52
+ /** Rewrite the file atomically, creating its directory on first use. */
53
+ private persist;
54
+ }
@@ -0,0 +1,162 @@
1
+ /** User-saved shortcut prompts, persisted in one private JSON file.
2
+ *
3
+ * The list belongs to the operator rather than to a session or a host, so it survives session
4
+ * switches, reconnects and machine restarts. Without a configured path the list still works for the
5
+ * current run, which is what tests and `--help` runs use.
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { dirname } from 'node:path';
9
+ import { ensureDirectory, readText, writePrivateFile } from "../storage/index.js";
10
+ import { errorText } from "../transport/wire.js";
11
+ /** Largest single saved prompt; a shortcut is meant to be reusable, not a document store. */
12
+ export const MAX_PROMPT_CHARS = 8 * 1024;
13
+ /** Most saved prompts one client keeps. */
14
+ export const MAX_SAVED_PROMPTS = 500;
15
+ /** On-disk shape this build writes. */
16
+ const FILE_VERSION = 1;
17
+ /** Read and persist shortcut prompts; an absent path keeps them in memory for this run.
18
+ *
19
+ * Mutations are queued: a save, edit or delete computes and writes inside the queue, so two rapid
20
+ * commands can never interleave their writes and leave the file disagreeing with the list.
21
+ */
22
+ export class PromptStore {
23
+ path;
24
+ items = [];
25
+ loaded = false;
26
+ /** Tail of the mutation queue; the next operation starts only after the previous one settled. */
27
+ queue = Promise.resolve();
28
+ /** Last read or write failure, shown by the saved-prompt picker. */
29
+ error;
30
+ constructor(path) {
31
+ this.path = path;
32
+ }
33
+ /** Entries in the order they were saved, oldest first. */
34
+ get list() { return this.items; }
35
+ /** Read the file once; a missing file is simply an empty list.
36
+ *
37
+ * A malformed document leaves the list empty and is reported through `error` rather than thrown,
38
+ * because a broken shortcuts file must not stop the client from starting.
39
+ * @returns Whether the result differs from the empty default, so a caller knows to republish.
40
+ */
41
+ async load() {
42
+ return this.enqueue(async () => {
43
+ if (this.path === undefined || this.loaded)
44
+ return false;
45
+ this.loaded = true;
46
+ try {
47
+ const raw = await readText(this.path);
48
+ if (raw === undefined)
49
+ return false;
50
+ this.items = promptsFrom(JSON.parse(raw));
51
+ this.error = undefined;
52
+ return this.items.length > 0;
53
+ }
54
+ catch (error) {
55
+ this.error = `Saved prompts could not be read: ${errorText(error)}`;
56
+ return true;
57
+ }
58
+ });
59
+ }
60
+ /** Add one prompt; a text already saved is returned instead of duplicated.
61
+ * @param value - Prompt text exactly as the user typed it.
62
+ * @returns The saved entry.
63
+ */
64
+ async save(value) {
65
+ return this.enqueue(async () => {
66
+ const text = checked(value);
67
+ const existing = this.items.find(item => item.text === text);
68
+ if (existing)
69
+ return existing;
70
+ if (this.items.length >= MAX_SAVED_PROMPTS)
71
+ throw new Error(`Saved prompts are limited to ${MAX_SAVED_PROMPTS} entries`);
72
+ const prompt = { id: randomUUID(), text };
73
+ await this.commit([...this.items, prompt]);
74
+ return prompt;
75
+ });
76
+ }
77
+ /** Replace one saved prompt's text.
78
+ * @param id - Entry identity.
79
+ * @param value - Replacement text.
80
+ * @returns False when the entry no longer exists.
81
+ */
82
+ async update(id, value) {
83
+ return this.enqueue(async () => {
84
+ const text = checked(value);
85
+ if (!this.items.some(item => item.id === id))
86
+ return false;
87
+ await this.commit(this.items.map(item => item.id === id ? { id, text } : item));
88
+ return true;
89
+ });
90
+ }
91
+ /** Drop one saved prompt.
92
+ * @param id - Entry identity.
93
+ * @returns False when it was already gone.
94
+ */
95
+ async remove(id) {
96
+ return this.enqueue(async () => {
97
+ const next = this.items.filter(item => item.id !== id);
98
+ if (next.length === this.items.length)
99
+ return false;
100
+ await this.commit(next);
101
+ return true;
102
+ });
103
+ }
104
+ /** Queue one operation behind everything already queued, whether or not the last one failed.
105
+ * @param task - Operation to run when it reaches the head of the queue.
106
+ * @returns The operation's own result or failure.
107
+ */
108
+ enqueue(task) {
109
+ const run = this.queue.then(task, task);
110
+ this.queue = run.then(() => undefined, () => undefined);
111
+ return run;
112
+ }
113
+ /** Apply a new list, restoring the previous one when the write fails, so memory follows the file. */
114
+ async commit(items) {
115
+ const previous = this.items;
116
+ this.items = items;
117
+ try {
118
+ await this.persist();
119
+ this.error = undefined;
120
+ }
121
+ catch (error) {
122
+ this.items = previous;
123
+ throw error;
124
+ }
125
+ }
126
+ /** Rewrite the file atomically, creating its directory on first use. */
127
+ async persist() {
128
+ if (this.path === undefined)
129
+ return;
130
+ await ensureDirectory(dirname(this.path));
131
+ await writePrivateFile(this.path, `${JSON.stringify({ version: FILE_VERSION, prompts: this.items }, null, 2)}\n`);
132
+ }
133
+ }
134
+ /** Trim and bound one submitted prompt.
135
+ * @param value - Raw text after the command.
136
+ * @returns The text to store.
137
+ */
138
+ function checked(value) {
139
+ const text = value.trim();
140
+ if (!text)
141
+ throw new Error('Type a prompt after /prompt');
142
+ if (text.length > MAX_PROMPT_CHARS)
143
+ throw new Error(`A saved prompt is limited to ${MAX_PROMPT_CHARS} characters`);
144
+ return text;
145
+ }
146
+ /** Validate the stored document, skipping entries this build cannot use.
147
+ * @param raw - Parsed JSON document.
148
+ * @returns The saved prompts, in file order.
149
+ */
150
+ function promptsFrom(raw) {
151
+ const document = raw;
152
+ if (!document || typeof document !== 'object' || !Array.isArray(document.prompts))
153
+ throw new Error('unsupported prompts file');
154
+ const out = [];
155
+ for (const entry of document.prompts) {
156
+ const item = entry;
157
+ if (!item || typeof item !== 'object' || typeof item.id !== 'string' || typeof item.text !== 'string' || !item.text.trim())
158
+ continue;
159
+ out.push({ id: item.id, text: item.text });
160
+ }
161
+ return out;
162
+ }
@@ -0,0 +1,45 @@
1
+ import { type ObjectValue } from '../transport/wire.ts';
2
+ /** The index the newest `keep` lines start at, moved forward past any split lifecycle span.
3
+ *
4
+ * A span opened before the cut and closed after it would be kept as a close without its begin, so the
5
+ * cut moves to just after that close and the check repeats: the newly dropped region may itself open a
6
+ * span that closes later. A span whose close never appears cannot be split, so it does not move the
7
+ * cut — that is the in-flight command the file exists to show.
8
+ * @param lines - Every line this process appended, oldest first.
9
+ * @param keep - How many of the newest lines the caller wants to keep.
10
+ * @returns Index of the first line to keep; always at most `lines.length - keep`.
11
+ */
12
+ export declare function compactCut(lines: readonly string[], keep?: number): number;
13
+ /** Serialized append-only trace; the file holds at most `KEEP_EVENTS` lines, fewer when a lifecycle span would be split.
14
+ *
15
+ * Writes are chained rather than awaited by the caller, because transitions are published from
16
+ * synchronous state updates. A failed write is remembered and reported on the next event instead of
17
+ * throwing into the controller.
18
+ */
19
+ export declare class TraceLog {
20
+ readonly path: string | undefined;
21
+ private queue;
22
+ private lines;
23
+ private directoryEnsured;
24
+ /** Last write failure, when the log stopped growing. */
25
+ error: string | undefined;
26
+ constructor(path: string | undefined);
27
+ /** Append one event; an absent path makes this a no-op. */
28
+ record(event: ObjectValue): void;
29
+ /** Wait for queued writes, so a shutdown does not lose the last events. */
30
+ settle(): Promise<void>;
31
+ /** Write one line, then bound the file when the cap is reached. */
32
+ private append;
33
+ /** Rewrite the file with the header and the newest kept lines, bounding its size.
34
+ *
35
+ * Older runs are not read back: `lines` is what this process appended, which bounds both the file
36
+ * and the read that rewrites it. The cut is pairing-aware, so a kept `begin` is never orphaned from
37
+ * its close and a kept close never loses its begin.
38
+ */
39
+ private compact;
40
+ }
41
+ /** Read a trace back, so a test or a diagnostic tool can assert what was recorded.
42
+ * @param path - Trace file to read.
43
+ * @returns Every non-empty line, oldest first.
44
+ */
45
+ export declare function readTrace(path: string): Promise<string[]>;