@gaunt-sloth/core 2.0.0-beta.1 → 2.0.0-beta.2
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/LICENSE +6 -6
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.js +55 -11
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/schema.js +46 -24
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +142 -1
- package/dist/config/shell-policy.js +114 -7
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/types.d.ts +19 -0
- package/dist/config/types.js.map +1 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +31 -0
- package/dist/core/GthAbstractAgent.js +159 -4
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +314 -18
- package/dist/core/GthAgentRunner.js +1032 -101
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/approvals/approvalRequest.d.ts +247 -0
- package/dist/core/approvals/approvalRequest.js +315 -0
- package/dist/core/approvals/approvalRequest.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +165 -9
- package/dist/core/approvals/grants.js +702 -57
- package/dist/core/approvals/grants.js.map +1 -1
- package/dist/core/plainToolIndication.d.ts +11 -1
- package/dist/core/plainToolIndication.js +25 -10
- package/dist/core/plainToolIndication.js.map +1 -1
- package/dist/core/shell/abstention.d.ts +1 -1
- package/dist/core/shell/abstention.js +65 -11
- package/dist/core/shell/abstention.js.map +1 -1
- package/dist/core/shell/alignment.d.ts +491 -0
- package/dist/core/shell/alignment.js +687 -0
- package/dist/core/shell/alignment.js.map +1 -0
- package/dist/core/shell/approvalCapture.d.ts +53 -41
- package/dist/core/shell/approvalCapture.js +0 -42
- package/dist/core/shell/approvalCapture.js.map +1 -1
- package/dist/core/shell/approvalStop.d.ts +121 -3
- package/dist/core/shell/approvalStop.js +153 -90
- package/dist/core/shell/approvalStop.js.map +1 -1
- package/dist/core/shell/hardline.d.ts +26 -4
- package/dist/core/shell/hardline.js +321 -16
- package/dist/core/shell/hardline.js.map +1 -1
- package/dist/core/shell/negotiation.d.ts +352 -63
- package/dist/core/shell/negotiation.js +295 -123
- package/dist/core/shell/negotiation.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +135 -0
- package/dist/core/shell/openWorld.js +572 -35
- package/dist/core/shell/openWorld.js.map +1 -1
- package/dist/core/shell/provenance.d.ts +91 -0
- package/dist/core/shell/provenance.js +136 -0
- package/dist/core/shell/provenance.js.map +1 -0
- package/dist/core/shell/rater.d.ts +394 -163
- package/dist/core/shell/rater.js +536 -238
- package/dist/core/shell/rater.js.map +1 -1
- package/dist/core/shell/raterHealth.d.ts +101 -0
- package/dist/core/shell/raterHealth.js +121 -0
- package/dist/core/shell/raterHealth.js.map +1 -0
- package/dist/core/shell/raterModel.d.ts +17 -8
- package/dist/core/shell/raterModel.js +11 -8
- package/dist/core/shell/raterModel.js.map +1 -1
- package/dist/core/shell/rejection.d.ts +20 -5
- package/dist/core/shell/rejection.js +18 -4
- package/dist/core/shell/rejection.js.map +1 -1
- package/dist/core/toolDisplay.d.ts +42 -0
- package/dist/core/toolDisplay.js +31 -0
- package/dist/core/toolDisplay.js.map +1 -1
- package/dist/core/types.d.ts +134 -16
- package/dist/core/types.js.map +1 -1
- package/dist/providers/configurationPassthrough.d.ts +36 -6
- package/dist/providers/configurationPassthrough.js +40 -28
- package/dist/providers/configurationPassthrough.js.map +1 -1
- package/dist/providers/openrouter.js +36 -6
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.js +21 -0
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js +103 -21
- package/dist/utils/binaryOutputUtils.js.map +1 -1
- package/dist/utils/fileUtils.d.ts +7 -1
- package/dist/utils/fileUtils.js +17 -3
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/globalConfigUtils.d.ts +5 -2
- package/dist/utils/globalConfigUtils.js +14 -3
- package/dist/utils/globalConfigUtils.js.map +1 -1
- package/dist/utils/vertexaiUtils.js +61 -3
- package/dist/utils/vertexaiUtils.js.map +1 -1
- package/package.json +4 -4
- package/schema/gsloth-config.schema.json +34 -0
|
@@ -1,4 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/negotiation
|
|
3
|
+
*
|
|
4
|
+
* [[EXT-29]] (spec §5) — **the bounded, visible argument between the agent and the rater** that
|
|
5
|
+
* `auto` conducts where `assisted` interrupts a person.
|
|
6
|
+
*
|
|
7
|
+
* A `destructive` rating at `auto` does not go to the human. It goes back to the *agent* as the
|
|
8
|
+
* refused call's tool result (§7), naming what would make the command acceptable (§5.2); the agent
|
|
9
|
+
* may narrow the command or justify the one it chose; the next call is rated again with the
|
|
10
|
+
* exchange in view (§5.1). This module holds the state that makes that a bounded exchange rather
|
|
11
|
+
* than a loop: the transcript, and the two counters that end it at a person.
|
|
12
|
+
*
|
|
13
|
+
* **It decides nothing about safety.** Every round is a full, independent rating by
|
|
14
|
+
* {@link import('./rater.js').mapVerdictToAction}; this only answers *"may another round be
|
|
15
|
+
* served, or is it a human's turn?"*.
|
|
16
|
+
*
|
|
17
|
+
* ## The lifetimes, which are the whole design
|
|
18
|
+
*
|
|
19
|
+
* - **The transcript outlives the consecutive counter** ([[EXT-108]]). A successful intervening
|
|
20
|
+
* tool call resets §5.3's counter and leaves the rounds standing, so the retry after it is rated
|
|
21
|
+
* as round 2 with the argument so far in view. §5.3's reset bounds a *stalled* negotiation, and a
|
|
22
|
+
* command run *because the rater asked for it* is that argument continuing rather than a new one:
|
|
23
|
+
* clearing the rounds there made the agent's compliance erase the record of the advice it was
|
|
24
|
+
* complying with, and demoted the retry to a round-1 rating whose explanation §5.1 withholds.
|
|
25
|
+
* The spec's warning about justification text accumulating across unbounded attempts is answered
|
|
26
|
+
* by the reachability bound rather than by the reset — the attempts are not unbounded.
|
|
27
|
+
* - **Only reaching a person clears the transcript** ({@link ShellNegotiationState.humanReached}),
|
|
28
|
+
* which is also the only thing that clears the reachability bound. The rounds a rating can see
|
|
29
|
+
* and the rejections counted against that bound are therefore the same set, always.
|
|
30
|
+
* - **The reachability bound is monotonic and a reset does not refill it.** See
|
|
31
|
+
* {@link MAX_REJECTIONS_BEFORE_HUMAN}.
|
|
32
|
+
*/
|
|
33
|
+
import type { AlignmentRound } from '#src/core/shell/alignment.js';
|
|
34
|
+
import type { RaterNegotiationRound } from '#src/core/shell/rater.js';
|
|
2
35
|
/**
|
|
3
36
|
* §5.3 — **three CONSECUTIVE rejections end the negotiation** and escalate to the human: the agent
|
|
4
37
|
* and the rater cannot agree, and that is a human's call, not a machine's.
|
|
@@ -18,10 +51,15 @@ export declare const MAX_CONSECUTIVE_REJECTIONS = 3;
|
|
|
18
51
|
* add one without a fresh decision. **This is that decision (Andrew, EXT-29), and it answers a
|
|
19
52
|
* different question than the one §5.3 asked.**
|
|
20
53
|
*
|
|
21
|
-
* §5.3's argument is about **safety**, and
|
|
22
|
-
* independently, the rater does not weaken with repetition, and
|
|
23
|
-
*
|
|
24
|
-
*
|
|
54
|
+
* §5.3's argument is about **safety**, and its core is untouched: every attempt is rated
|
|
55
|
+
* independently, the rater does not weaken with repetition, and every deterministic floor is
|
|
56
|
+
* recomputed from the RAW command each round, so no amount of argument unlocks one. What
|
|
57
|
+
* [[EXT-108]] changed is that a reset no longer clears the rounds with the counter, so the
|
|
58
|
+
* justification channel §5.1 opens at round 2 stays open across an approved call — deliberately,
|
|
59
|
+
* because the round after a call the rater itself asked for is the same argument continuing. That
|
|
60
|
+
* is the one channel able to LOWER an outcome, which is why what bounds it matters: this count,
|
|
61
|
+
* which no reset refills, is what ends an argument nobody is winning. Nothing here contradicts
|
|
62
|
+
* §5.3, and this bound must never be described as a security control.
|
|
25
63
|
*
|
|
26
64
|
* The question §5.3 never asked is **reachability**: does the human terminus fire at all? Under its
|
|
27
65
|
* own reset predicate it does not. An agent that alternates one approved `ls` with one rejection
|
|
@@ -83,54 +121,43 @@ export declare const NEGOTIATION_USER_MESSAGE_RETENTION = 10;
|
|
|
83
121
|
* | event | transcript | consecutive | since-human |
|
|
84
122
|
* |---|---|---|---|
|
|
85
123
|
* | a rejection ({@link recordRejection}) | append | +1 | +1 |
|
|
86
|
-
* | the gate approved a call ({@link noteProgress}) | **
|
|
124
|
+
* | the gate approved a call ({@link noteProgress}) | **kept** | 0 | unchanged |
|
|
87
125
|
* | a human was reached ({@link humanReached}) | cleared | 0 | **0** |
|
|
88
126
|
* | the run halted ({@link humanReached}) | cleared | 0 | 0 |
|
|
89
127
|
*/
|
|
90
128
|
export declare class ShellNegotiationState {
|
|
91
|
-
/** §5.1's transcript: every round
|
|
129
|
+
/** §5.1's transcript: every round since a person was last involved, oldest first. */
|
|
92
130
|
private rounds;
|
|
93
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* §5.3's consecutive-rejection count. Reset by an approved call, which [[EXT-108]] stopped taking
|
|
133
|
+
* the rounds with it — so this is the shorter of the two lifetimes, not a shared one.
|
|
134
|
+
*/
|
|
94
135
|
private consecutive;
|
|
95
136
|
/** The reachability bound's count. Cleared ONLY by {@link humanReached}. */
|
|
96
137
|
private sinceHuman;
|
|
97
138
|
/** §5.1's last user messages, oldest first, capped at {@link NEGOTIATION_USER_MESSAGE_RETENTION}. */
|
|
98
139
|
private userMessages;
|
|
99
140
|
/**
|
|
100
|
-
* §
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*
|
|
107
|
-
* An empty negotiation has no privileged spelling on the builder's side, so this always returns an
|
|
108
|
-
* object and never `undefined`: a cleared transcript IS the round-1 case, with no `if` at the call
|
|
109
|
-
* site — which is exactly what makes §5.6's *"a cleared transcript means a round-1 context"* fall
|
|
110
|
-
* out of the reset rather than out of a second branch that could disagree with it.
|
|
111
|
-
*
|
|
112
|
-
* **Everything except the command is admitted from round 2, never at round 1**, and the transcript
|
|
113
|
-
* is what decides which round this is. §5.1 is unambiguous — *"Round 1 sees the command alone —
|
|
114
|
-
* nothing else"* — and §5.6 spells out the consequence for the round right after a reset: *"the
|
|
115
|
-
* command and nothing else — no transcript, no user messages, because that is what round 1 means"*.
|
|
141
|
+
* [[EXT-106]] §4.6 — whether {@link retainedUserMessages} may hand this session's human turns out
|
|
142
|
+
* as **the user's own words**. See {@link admitUserProvenance}; `false` until something positively
|
|
143
|
+
* establishes otherwise, which is the whole of its safety.
|
|
144
|
+
*/
|
|
145
|
+
private userProvenanceAdmitted;
|
|
146
|
+
/**
|
|
147
|
+
* [[EXT-127]] — **the checker's own earlier rounds, for the alignment check about to be made.**
|
|
116
148
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* for the agent or for anything that has injected into the agent's context. Withholding it until
|
|
123
|
-
* round 2 is what the spec's ordering buys, and a round-1 context is byte-identical to a plain
|
|
124
|
-
* rating because of it.
|
|
149
|
+
* The rounds it returns are the transcript's, with each round's alignment decision attached where
|
|
150
|
+
* the checker made one. What §5.1's withholding rules used to govern here is gone with the flat
|
|
151
|
+
* context they governed: the classifier now sees the command alone at every round, and the
|
|
152
|
+
* checker's context is assembled across message roles by
|
|
153
|
+
* {@link import('./alignment.js').buildAlignmentMessages} rather than handed over as a block.
|
|
125
154
|
*
|
|
126
|
-
* The
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* it arriving on. A justification the agent supplies again with its next attempt is admitted then,
|
|
131
|
-
* because by then there is a rejection for it to answer.
|
|
155
|
+
* **The user's words are deliberately NOT here.** They come from
|
|
156
|
+
* {@link retainedUserMessages} — the gated provenance channel — and never from this method or
|
|
157
|
+
* from {@link noteUserMessages}' store. Routing them through a "context for the next call"
|
|
158
|
+
* accessor is exactly how the gate would come to be bypassed by a caller doing the obvious thing.
|
|
132
159
|
*/
|
|
133
|
-
|
|
160
|
+
alignmentRounds(): readonly AlignmentRound[];
|
|
134
161
|
/**
|
|
135
162
|
* Record a rejected round and decide whether another may be served.
|
|
136
163
|
*
|
|
@@ -142,16 +169,24 @@ export declare class ShellNegotiationState {
|
|
|
142
169
|
*/
|
|
143
170
|
recordRejection(round: RaterNegotiationRound): NegotiationVerdict;
|
|
144
171
|
/**
|
|
145
|
-
* §5.3 — a tool call the gate let through. Resets the consecutive counter
|
|
146
|
-
*
|
|
172
|
+
* §5.3 — a tool call the gate let through. Resets the consecutive counter and **nothing else**:
|
|
173
|
+
* the rounds stand, and so does the reachability bound.
|
|
147
174
|
*
|
|
148
|
-
* **
|
|
149
|
-
* reset
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
175
|
+
* **The counter reset is §5.3's and is sound; taking the rounds with it was not** ([[EXT-108]]).
|
|
176
|
+
* The reset exists to bound a *stalled* negotiation — an agent that went away, gathered
|
|
177
|
+
* information and came back better informed is making progress, not ping-pong. But the approved
|
|
178
|
+
* call is very often the agent doing exactly what the rejection told it to do, and clearing the
|
|
179
|
+
* rounds there makes that compliance erase the record of the advice: the retry is then rated with
|
|
180
|
+
* an empty transcript, which {@link contextFor} correctly reads as round 1 and so withholds the
|
|
181
|
+
* justification and the user messages from. **Measured**: the rater refused `git reset --hard`
|
|
182
|
+
* advising a stash first, the agent stashed, and the rater then advised a stash twice more —
|
|
183
|
+
* recommending a thing already done, because on the evidence it was given it could not know.
|
|
184
|
+
*
|
|
185
|
+
* **What replaces the clearing as a bound is the count this deliberately leaves standing.** Every
|
|
186
|
+
* appended round increments {@link NegotiationCounters.rejectionsSinceHuman}, which no reset
|
|
187
|
+
* refills, so a transcript still cannot grow past {@link MAX_REJECTIONS_BEFORE_HUMAN} rounds
|
|
188
|
+
* before {@link humanReached} clears the lot. A cap on the retained rounds would bound something
|
|
189
|
+
* that is already bounded, and would re-introduce the erasure on a longer fuse.
|
|
155
190
|
*
|
|
156
191
|
* **"Approved" is what the gate can observe, and it is not quite §5.3's "successful".** The
|
|
157
192
|
* decision site sees whether a call was allowed to run, never whether it then exited zero — and
|
|
@@ -184,6 +219,48 @@ export declare class ShellNegotiationState {
|
|
|
184
219
|
* thing they said, and it was said most recently.
|
|
185
220
|
*/
|
|
186
221
|
noteUserMessages(messages: readonly string[]): void;
|
|
222
|
+
/**
|
|
223
|
+
* [[EXT-106]] §4.6 — **the user's own words, for the provenance carve-out that lifts the
|
|
224
|
+
* open-world floor.** A snapshot, oldest first, of the whole retained window.
|
|
225
|
+
*
|
|
226
|
+
* **This is deliberately NOT {@link contextFor}, and the difference is the point.** That function
|
|
227
|
+
* returns `userMessages: []` at round 1 by design — §5.1's *"round 1 sees the command alone"* —
|
|
228
|
+
* and round 1 is exactly the round the carve-out exists to act on: the user asks for a fetch, the
|
|
229
|
+
* agent proposes it, and there has been no rejection for a round 2 to exist. §5.1 bounds what the
|
|
230
|
+
* **rater** may see, and the floor is not the rater. Reading `contextFor()` here would make the
|
|
231
|
+
* carve-out fire only after the command had already been refused once, i.e. never in the case it
|
|
232
|
+
* was built for.
|
|
233
|
+
*
|
|
234
|
+
* The window is cumulative across the turns of a thread (capped and de-duplicated by
|
|
235
|
+
* {@link noteUserMessages}), so a host named in an earlier turn still carves a command proposed in
|
|
236
|
+
* a later one. {@link clear} drops it with the thread.
|
|
237
|
+
*
|
|
238
|
+
* **Empty until {@link admitUserProvenance} says otherwise**, which is why this is the provenance
|
|
239
|
+
* window rather than {@link noteUserMessages}'s store: the store also feeds §5.1's negotiation
|
|
240
|
+
* context, a different question with a different reader, and one that a command's fetched content
|
|
241
|
+
* is legitimately part of. What must never be answered *"yes, the human said this"* is this
|
|
242
|
+
* accessor, so the gate lives on it and defaults to refusing.
|
|
243
|
+
*/
|
|
244
|
+
retainedUserMessages(): readonly string[];
|
|
245
|
+
/**
|
|
246
|
+
* [[EXT-106]] §4.6 — **declare whether this session's human turns are the user's own words**, and
|
|
247
|
+
* therefore whether {@link retainedUserMessages} may return them at all.
|
|
248
|
+
*
|
|
249
|
+
* A human message the product SYNTHESISED from content it fetched must never enter the provenance
|
|
250
|
+
* window: `review` and `pr` hand the agent a diff and a PR description that gaunt-sloth went and
|
|
251
|
+
* got, and the review prompt tells the agent to examine them. Reading the same bytes as the user's
|
|
252
|
+
* verbatim words would make the product contradict itself about identical input — and the words
|
|
253
|
+
* are written by whoever opened the pull request.
|
|
254
|
+
*
|
|
255
|
+
* **It defaults to `false` and the caller must positively establish otherwise**, so a surface that
|
|
256
|
+
* never calls this — or a future driver nobody has classified — floors exactly as it did before
|
|
257
|
+
* the carve-out existed. The one caller is
|
|
258
|
+
* {@link import('../GthAgentRunner.js').GthAgentRunner.init}, which decides it from the CLI verb
|
|
259
|
+
* ({@link import('../../config/shell-policy.js').commandCarriesUserProvenance}) and never from
|
|
260
|
+
* anything inside a message: those bytes are attacker-controlled, so a marker in them can be
|
|
261
|
+
* forged by the text it is supposed to classify.
|
|
262
|
+
*/
|
|
263
|
+
admitUserProvenance(admitted: boolean): void;
|
|
187
264
|
/**
|
|
188
265
|
* §6 — the rounds to show the human, oldest first. A snapshot: the caller holds it across the
|
|
189
266
|
* {@link humanReached} that immediately follows, and nothing it holds may change underneath it.
|
|
@@ -224,6 +301,132 @@ export interface NegotiationRow {
|
|
|
224
301
|
voice: NegotiationVoice;
|
|
225
302
|
text: string;
|
|
226
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* §5.4 — **one round of the argument, handed to a surface the moment it happens.**
|
|
306
|
+
*
|
|
307
|
+
* The round travels raw rather than rendered, so each surface lays it out with
|
|
308
|
+
* {@link renderNegotiationRows} at its own terminal width — the same renderer, and the same
|
|
309
|
+
* voice-tagged rows, that the escalation prompt draws. A pre-rendered string would be the second
|
|
310
|
+
* renderer §5.4 exists to prevent, and it could not be bound to a width the runner cannot know.
|
|
311
|
+
*/
|
|
312
|
+
export interface LiveNegotiationRound {
|
|
313
|
+
/** The round: the command proposed, the agent's justification, the rater's outcome and reason. */
|
|
314
|
+
round: RaterNegotiationRound;
|
|
315
|
+
/**
|
|
316
|
+
* **How many rounds of the transcript precede this one, counting from zero.**
|
|
317
|
+
*
|
|
318
|
+
* It is passed rather than derived because the two facts {@link renderNegotiationRows} marks —
|
|
319
|
+
* *this was rated on the command alone* (§5.1's round 1) and *this is round N* — are properties
|
|
320
|
+
* of a POSITION IN THE TRANSCRIPT, and a caller handing over one round at a time has taken the
|
|
321
|
+
* array's length away as a source for them. A live block that derived them would call every
|
|
322
|
+
* round the first one.
|
|
323
|
+
*
|
|
324
|
+
* For a rejection this is its own index: the k-th rejection is at `k - 1`, which numbers it
|
|
325
|
+
* `Round k`. For {@link agreed} it is the whole rejection count, because the approving round
|
|
326
|
+
* follows all of them — but it is NOT numbered from it; see {@link agreed}.
|
|
327
|
+
*/
|
|
328
|
+
position: number;
|
|
329
|
+
/**
|
|
330
|
+
* [[TUI-C69]] §5.4 — **the rater agreed, so this round ends the argument rather than continuing
|
|
331
|
+
* it, and it takes no round number.**
|
|
332
|
+
*
|
|
333
|
+
* **A number here would be a number two different commands answer to.** The transcript holds
|
|
334
|
+
* rejections and nothing else — {@link ShellNegotiationState.recordRejection} is its only writer
|
|
335
|
+
* — so an approved call never joins it, and the next rejection takes the position this round was
|
|
336
|
+
* displayed at. Numbered, the live view would show two `Round 2` rows, and the escalation prompt
|
|
337
|
+
* (which renders that same transcript) would give `Round 2` to a third command again. The two
|
|
338
|
+
* views exist to be one account of one argument, so the number belongs to whatever the transcript
|
|
339
|
+
* says it belongs to, and the round that is not in the transcript is labelled instead of counted.
|
|
340
|
+
*
|
|
341
|
+
* Left unset for a rejection.
|
|
342
|
+
*/
|
|
343
|
+
agreed?: boolean;
|
|
344
|
+
/**
|
|
345
|
+
* [[TUI-C69]] §5.4 — **the command that ended the argument is not the command that started it**,
|
|
346
|
+
* so the row says the rater ACCEPTED a revision rather than AGREED to what it refused.
|
|
347
|
+
*
|
|
348
|
+
* Both are ways an argument ends and both are held, because §5.3 counts either as progress. They
|
|
349
|
+
* are not the same claim, and only one of them can be made about a given command:
|
|
350
|
+
*
|
|
351
|
+
* - unset — the rater refused this exact command and has now passed it. *Agreed.*
|
|
352
|
+
* - set — the approved command is not one the transcript holds. The agent narrowed
|
|
353
|
+
* `git reset --hard origin/main` to `git reset --soft HEAD~2` and the rater passed the new one;
|
|
354
|
+
* or it went off to run something else entirely first. *Accepted*, which says what happened
|
|
355
|
+
* without claiming the rater ever argued about this command.
|
|
356
|
+
*
|
|
357
|
+
* The distinction is the whole of the wrong-output half of the defect this fixes: the label used
|
|
358
|
+
* to read `Agreed: ls src` over a command nobody had refused, which is a false statement about
|
|
359
|
+
* the auto-rater printed in the chrome of the thing asking the user to trust it.
|
|
360
|
+
*
|
|
361
|
+
* Meaningful only with {@link agreed}; ignored on a rejection, which is numbered.
|
|
362
|
+
*/
|
|
363
|
+
revised?: boolean;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* §5.4/§5.5 — **the surface that is showing this negotiation while it happens.**
|
|
367
|
+
*
|
|
368
|
+
* Wiring one is a surface saying *"I have a live display, and a person is looking at it"*, and it
|
|
369
|
+
* is what both halves of the visible negotiation are keyed on:
|
|
370
|
+
*
|
|
371
|
+
* - each round is handed to {@link round} as it is decided, rather than only at an escalation;
|
|
372
|
+
* - a negotiated approval is held for {@link NEGOTIATED_APPROVAL_COOLDOWN_MS} before it takes
|
|
373
|
+
* effect.
|
|
374
|
+
*
|
|
375
|
+
* **A surface that wires nothing neither renders nor sleeps**, which is the whole reason the two
|
|
376
|
+
* are one seam. An `exec` or CI run has nobody to show an approval to, so an 800 ms hold there
|
|
377
|
+
* would tax every headless run and every gate to display something to no one.
|
|
378
|
+
*/
|
|
379
|
+
export interface NegotiationDisplay {
|
|
380
|
+
/** One round of the exchange, the moment the gate decided it. */
|
|
381
|
+
round(event: LiveNegotiationRound): void;
|
|
382
|
+
/**
|
|
383
|
+
* **The exchange is over** — a person was reached, the run halted, the turn ended, or a new one
|
|
384
|
+
* began. Called wherever {@link ShellNegotiationState.humanReached} is spent, and also at the end
|
|
385
|
+
* of a turn, so a surface holding the rounds drops them whenever there is no longer a live
|
|
386
|
+
* argument for them to be the state of.
|
|
387
|
+
*
|
|
388
|
+
* **The turn-end call is the one a converging negotiation depends on.** An escalation clears
|
|
389
|
+
* itself, because a person is reached; a negotiation that SUCCEEDS reaches nobody, so without it
|
|
390
|
+
* the rounds stay pinned through the idle period after the turn — the period in which the user is
|
|
391
|
+
* reading the result and typing the next thing.
|
|
392
|
+
*
|
|
393
|
+
* It matters most at an ESCALATION, where the prompt is about to render the whole argument
|
|
394
|
+
* itself: a live copy left standing above it puts the same exchange on an unscrollable screen
|
|
395
|
+
* twice, and the rows it spends are rows the dialog needs.
|
|
396
|
+
*
|
|
397
|
+
* **Optional, because an append-only surface cannot honour it.** The readline session prints each
|
|
398
|
+
* round as a line of scrollback; there is nothing to clear, and a surface that implements nothing
|
|
399
|
+
* simply keeps what it printed.
|
|
400
|
+
*
|
|
401
|
+
* *That is a limitation rather than a justification*, and the difference is visible at an
|
|
402
|
+
* escalation: the readline prompt re-renders the whole transcript, so a user who has just watched
|
|
403
|
+
* rounds 1-3 scroll past is shown rounds 1-3 again inside the prompt. Nothing here can prevent
|
|
404
|
+
* that — you cannot unprint scrollback — and the duplicate is the cheaper half of the trade,
|
|
405
|
+
* since the prompt must show what it is asking about. Recorded so the next reader meets the cost
|
|
406
|
+
* rather than the excuse.
|
|
407
|
+
*/
|
|
408
|
+
end?(): void;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* §5.5 — **the minimum interval a negotiated approval is visible before it takes effect.**
|
|
412
|
+
*
|
|
413
|
+
* It must never be described or relied upon as an opportunity to evaluate the command: nobody
|
|
414
|
+
* reads a command in 800 ms, and a design that assumed they did would be resting a safety property
|
|
415
|
+
* on a glance. What it buys is that the approving round exists on screen as its own event, rather
|
|
416
|
+
* than being drawn and overwritten by the tool's own output in the same frame.
|
|
417
|
+
*
|
|
418
|
+
* **It is NOT a guaranteed abort window on either surface**, and the claim must not be restored
|
|
419
|
+
* without the mechanism: on the event/TUI path an abort during the interval does stop the tool,
|
|
420
|
+
* but because LangGraph refuses an already-aborted signal rather than because this code re-checks
|
|
421
|
+
* one; on the plain/readline path no signal is threaded and the Esc handler is torn down before
|
|
422
|
+
* the interval begins, so the command runs. See `GthAgentRunner.showNegotiatedApproval`, which
|
|
423
|
+
* carries the full statement of what is true where.
|
|
424
|
+
*
|
|
425
|
+
* **A minimum visible interval, not a delay added to a finished decision.** The real window is
|
|
426
|
+
* longer at both ends — the rater call's own latency precedes it, and the tool takes time to start
|
|
427
|
+
* after it — so this bounds the short end of a window that mostly already exists.
|
|
428
|
+
*/
|
|
429
|
+
export declare const NEGOTIATED_APPROVAL_COOLDOWN_MS = 800;
|
|
227
430
|
/**
|
|
228
431
|
* Terminal rows one element of a round — the command, the justification, the rater's answer — may
|
|
229
432
|
* occupy on a screen before the rest of it is elided.
|
|
@@ -231,10 +434,10 @@ export interface NegotiationRow {
|
|
|
231
434
|
* **MEASURED, and it is the height bound this block needs rather than a cap on rounds.** At 80
|
|
232
435
|
* columns three rounds of paragraph-length argument cost 37 rows, of which one round was 12; the
|
|
233
436
|
* whole prompt was 64 rows against a 20-row budget, so the human saw the command and the verdict
|
|
234
|
-
* and neither the later rounds nor the menu line. A cap on the NUMBER of rounds
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* element keeps every round structurally on the screen, which is what §5.6 calls the most important
|
|
437
|
+
* and neither the later rounds nor the menu line. A cap on the NUMBER of rounds does not answer
|
|
438
|
+
* that — the measured case was three rounds, exactly what {@link NEGOTIATION_MAX_ROUNDS_SHOWN}
|
|
439
|
+
* allows a screen, and it still cost 37 rows — because the cost is per row, not per round. Bounding
|
|
440
|
+
* each element keeps every round structurally on the screen, which is what §5.6 calls the most important
|
|
238
441
|
* thing on it, and pays for it out of the tail of a paragraph the reader was never going to need in
|
|
239
442
|
* full: the archive keeps every round whole ([[TUI-C27]]'s capture is per rating call).
|
|
240
443
|
*
|
|
@@ -243,10 +446,12 @@ export interface NegotiationRow {
|
|
|
243
446
|
*/
|
|
244
447
|
export declare const NEGOTIATION_MAX_ROWS_PER_ELEMENT = 2;
|
|
245
448
|
/**
|
|
246
|
-
* Rounds a screen shows, newest last.
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
449
|
+
* Rounds a screen shows, newest last. **Binding rather than a backstop, since [[EXT-108]]**: an
|
|
450
|
+
* approved call no longer clears the transcript, so an agent that makes progress between refusals
|
|
451
|
+
* reaches a person carrying every round since the last one — up to
|
|
452
|
+
* {@link MAX_REJECTIONS_BEFORE_HUMAN} of them — and an unscrollable prompt cannot grow a section
|
|
453
|
+
* that long. What is dropped is said out loud in the heading, which carries the true count, and the
|
|
454
|
+
* markers keyed on a position in the transcript stay keyed on it across the slice.
|
|
250
455
|
*/
|
|
251
456
|
export declare const NEGOTIATION_MAX_ROUNDS_SHOWN = 3;
|
|
252
457
|
/**
|
|
@@ -278,18 +483,21 @@ export declare const NEGOTIATION_MAX_ROUNDS_SHOWN = 3;
|
|
|
278
483
|
* and each of these lied toward approving, at the moment a human was deciding whether to overrule
|
|
279
484
|
* a refusal.
|
|
280
485
|
*
|
|
281
|
-
* - **`attempts`
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
486
|
+
* - **`attempts` counts the argument; the rounds below it are a slice of that argument.** A screen
|
|
487
|
+
* shows at most {@link NEGOTIATION_MAX_ROUNDS_SHOWN} of them, so the heading has to report what
|
|
488
|
+
* happened rather than what fits, and the rounds have to be numbered from the true attempt
|
|
489
|
+
* number. The caller's `rejectionsSinceHuman` is that count. [[EXT-108]] brought it into
|
|
490
|
+
* agreement with `rounds.length` for a caller holding the whole transcript — an approved call no
|
|
491
|
+
* longer erases rounds, and one event clears both — so what this parameter still guards is the
|
|
492
|
+
* slice, plus the `Math.max` below, which stops a stale or smaller number making the block claim
|
|
493
|
+
* less argument than it is about to print. Omit it and this falls back to `rounds.length`, the
|
|
494
|
+
* honest reading of a caller that has no better number.
|
|
287
495
|
* - **The last round IS the pending rating, not a prior one.** {@link
|
|
288
496
|
* ShellNegotiationState.recordRejection} appends before either bound is tested, deliberately, so
|
|
289
497
|
* that the rating being escalated is on the transcript the human sees. The old heading called
|
|
290
498
|
* them all prior rounds, which both under-reported the argument and put the pending command on
|
|
291
499
|
* the screen twice with nothing saying they were the same call.
|
|
292
|
-
* - **The first round was rated on the command alone.**
|
|
500
|
+
* - **The first round was rated on the command alone.** An EMPTY transcript IS the round-1 case
|
|
293
501
|
* (see {@link ShellNegotiationState.contextFor}), so §5.1 withheld the justification and the user
|
|
294
502
|
* messages from that rating — while {@link ShellNegotiationState.recordRejection} stores the
|
|
295
503
|
* justification the agent supplied whatever the round. Printed unmarked, the round reads as a
|
|
@@ -311,6 +519,87 @@ export declare function renderNegotiationRows(rounds: readonly RaterNegotiationR
|
|
|
311
519
|
* ShellNegotiationState.counters}, read BEFORE the escalation spends it.
|
|
312
520
|
*/
|
|
313
521
|
attempts?: number;
|
|
522
|
+
/**
|
|
523
|
+
* [[TUI-C69]] §5.4 — **which block this is, and it changes three things.**
|
|
524
|
+
*
|
|
525
|
+
* - `escalation` (the default, and every caller that predates §5.4's live render) — the whole
|
|
526
|
+
* transcript, put in front of a person who is about to rule on it. It opens with the heading
|
|
527
|
+
* that counts the argument, and its last round IS the request being ruled on.
|
|
528
|
+
* - `live` — one round, drawn the moment it happened. **Nobody has been asked anything**, so
|
|
529
|
+
* the `(this request)` marker would be a lie, and a heading counting the argument would be
|
|
530
|
+
* re-printed on every round. The context sentence is drawn once, over the round that opens
|
|
531
|
+
* the exchange.
|
|
532
|
+
*
|
|
533
|
+
* What does NOT change is everything the rows say: the same labels, the same voices, the same
|
|
534
|
+
* width binding and the same row bound. The live view and the escalation prompt are one
|
|
535
|
+
* renderer precisely so the exchange a person watched and the exchange they are shown cannot
|
|
536
|
+
* be two different accounts of it.
|
|
537
|
+
*/
|
|
538
|
+
mode?: 'escalation' | 'live';
|
|
539
|
+
/**
|
|
540
|
+
* [[TUI-C69]] §5.4 — the transcript position of `rounds[0]`, counting from zero, when the
|
|
541
|
+
* CALLER took the slice. Omitted, this block takes the slice itself and knows the answer.
|
|
542
|
+
*
|
|
543
|
+
* It exists because the markers below are properties of a position in the transcript rather
|
|
544
|
+
* than of a rating, and `rounds.length` stops being able to supply one the moment a caller
|
|
545
|
+
* hands over a single round.
|
|
546
|
+
*/
|
|
547
|
+
from?: number;
|
|
548
|
+
/**
|
|
549
|
+
* [[TUI-C69]] §5.4 — **this live round is the rater AGREEING**, which ends the argument
|
|
550
|
+
* instead of being another attempt in it, so it is labelled rather than numbered.
|
|
551
|
+
*
|
|
552
|
+
* See {@link LiveNegotiationRound.agreed} for why it takes no number: the approved call never
|
|
553
|
+
* joins the transcript, so any number it were given is the one the next rejection then takes —
|
|
554
|
+
* and the escalation prompt, which renders the transcript, would hand that number to a
|
|
555
|
+
* different command. `live` only; the escalation block renders the transcript, which contains
|
|
556
|
+
* no approved rounds to label.
|
|
557
|
+
*/
|
|
558
|
+
agreed?: boolean;
|
|
559
|
+
/**
|
|
560
|
+
* [[TUI-C69]] §5.4 — with {@link agreed}, whether the approved command is one the transcript
|
|
561
|
+
* never held, which changes the label from *Agreed* to *Accepted*. See
|
|
562
|
+
* {@link LiveNegotiationRound.revised}. `live` + `agreed` only.
|
|
563
|
+
*/
|
|
564
|
+
revised?: boolean;
|
|
565
|
+
}): NegotiationRow[];
|
|
566
|
+
/**
|
|
567
|
+
* [[TUI-C69]] §5.4 — **the whole live exchange as a bounded screen**, for a surface that redraws a
|
|
568
|
+
* pinned region rather than appending to scrollback.
|
|
569
|
+
*
|
|
570
|
+
* ## Why this exists rather than the caller looping over {@link renderNegotiationRows}
|
|
571
|
+
*
|
|
572
|
+
* **A cap applied one round at a time is not a cap.** The escalation prompt hands the renderer its
|
|
573
|
+
* whole transcript, so `slice(-NEGOTIATION_MAX_ROUNDS_SHOWN)` bounds it; a live surface that
|
|
574
|
+
* rendered each round as it arrived and kept the output handed the renderer a ONE-element array
|
|
575
|
+
* every time, where that slice is an identity operation, and then accumulated the results. The
|
|
576
|
+
* bound was still in the code and applied to nothing. **Measured**: a nine-round argument cost 46
|
|
577
|
+
* rows at 80 columns that way, against 16 for the same argument in the prompt.
|
|
578
|
+
*
|
|
579
|
+
* That is not a tidiness problem. The Ink surface pins this inside a `flexShrink={0}` dock —
|
|
580
|
+
* [[TUI-C75]]'s governing constraint is that the dialog does not scroll, so rows spent here are
|
|
581
|
+
* taken from the conversation, and past a couple of dozen they push the input prompt off the
|
|
582
|
+
* bottom of an 80×24 terminal. Every other occupant of that dock is bounded; this was the one that
|
|
583
|
+
* was not.
|
|
584
|
+
*
|
|
585
|
+
* ## What it shows
|
|
586
|
+
*
|
|
587
|
+
* The newest {@link NEGOTIATION_MAX_ROUNDS_SHOWN} rounds, **unconditionally** — a screen is what
|
|
588
|
+
* this is for, so unlike {@link renderNegotiationRows} it does not let an absent `width` mean "show
|
|
589
|
+
* everything". A caller that wants the whole exchange unbounded wants the escalation renderer.
|
|
590
|
+
*
|
|
591
|
+
* Each round keeps the number of its own position in the transcript, so the numbers a watcher sees
|
|
592
|
+
* are the numbers the escalation prompt will use for the same rounds even after the earlier ones
|
|
593
|
+
* scroll out of this window — and an `agreed` round is labelled rather than numbered, for the
|
|
594
|
+
* reason {@link LiveNegotiationRound.agreed} gives.
|
|
595
|
+
*
|
|
596
|
+
* The context sentence is drawn **every time** rather than only over round one. Once the window
|
|
597
|
+
* slides, the round that carried it is gone, and the alternative is a bare `Round 7:` heading
|
|
598
|
+
* nothing — an unattributed command sitting in the chrome of a tool that is asking the user to
|
|
599
|
+
* trust it. One row is the right price for saying whose argument this is.
|
|
600
|
+
*/
|
|
601
|
+
export declare function renderLiveNegotiationRows(rounds: readonly LiveNegotiationRound[], options?: {
|
|
602
|
+
width?: number;
|
|
314
603
|
}): NegotiationRow[];
|
|
315
604
|
/**
|
|
316
605
|
* The same transcript as one string, for a consumer with no screen to lay it out on: §6.2's
|