@rmrdeveloper/sideroom-pi 8.9.0 → 8.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -45
- package/extensions/ask/index.ts +3 -2
- package/extensions/explain/guard.ts +14 -12
- package/extensions/explain/model.ts +4 -3
- package/extensions/guidelines/review.ts +63 -18
- package/extensions/jev/client.ts +43 -8
- package/extensions/jev/context.ts +327 -0
- package/extensions/jev/guard.ts +92 -25
- package/extensions/jev/index.ts +2 -2
- package/extensions/jev/model.ts +97 -16
- package/extensions/jev/ui.ts +1 -0
- package/extensions/persona/catalog.ts +2 -2
- package/extensions/persona/prompt.ts +2 -2
- package/extensions/shared/file-path.ts +26 -1
- package/extensions/shared/review-note.ts +14 -0
- package/extensions/shared/settle.ts +50 -0
- package/package.json +3 -3
- package/skills/sideroom-architecture/SKILL.md +12 -5
- package/skills/sideroom-grill/SKILL.md +10 -2
- package/skills/sideroom-persona/SKILL.md +34 -24
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ Sideroom answers each one with a small, opinionated surface:
|
|
|
55
55
|
| Invisible monorepo skills | `monorepo-skills` — trusted child folders join Pi's available skill list |
|
|
56
56
|
| Vague plans | `sideroom-grill` — an interview that settles the words before the work |
|
|
57
57
|
| Unapplied guidelines | `sideroom_rules` — mechanical checks that block or flag the lines you add |
|
|
58
|
-
| Unchecked judgment rules | `jev` — asks a decision model about
|
|
59
|
-
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of
|
|
58
|
+
| Unchecked judgment rules | `jev` — asks a decision model about guide rules with visible evidence |
|
|
59
|
+
| Unclear answers | `sideroom_persona` — one voice: direct, plain, and free of unnecessary technical words |
|
|
60
60
|
| Premature completion | `sideroom_done` — steers back to the project's check command before finishing |
|
|
61
61
|
| Work left unexplained | `explain` — offers a walkthrough and how to test it once the work settles |
|
|
62
62
|
|
|
@@ -140,9 +140,13 @@ project are labelled `external`. A new session starts clean.
|
|
|
140
140
|
|
|
141
141
|
An on-demand skill for fuzzy plans. It interviews you in `sideroom_ask`
|
|
142
142
|
rounds — reading the codebase first so it never asks what the code already
|
|
143
|
-
says — until you share one understanding.
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
says — until you share one understanding. Its recommendations favor choices
|
|
144
|
+
that are easy to maintain and meet known growth needs over the cheapest initial
|
|
145
|
+
build. It names the upfront cost; if a missing fact could change the choice,
|
|
146
|
+
it asks for that fact before recommending a solution. Resolved terms land in
|
|
147
|
+
`CONTEXT.md` the moment they resolve. Only hard-to-reverse, surprising choices
|
|
148
|
+
with a real trade-off qualify for a record under `docs/adr/`; the user is asked
|
|
149
|
+
first. Everything else stays in the conversation, ready to become a
|
|
146
150
|
spec or an implementation. Vocabulary sharpening during the rounds follows
|
|
147
151
|
the companion `sideroom-domain-modeling` skill, which also runs on its own
|
|
148
152
|
whenever the words — not the plan — are the problem. And when there is no
|
|
@@ -189,19 +193,24 @@ written, no config read: the catalog ships with the package.
|
|
|
189
193
|
|
|
190
194
|
### Jev — judgment where no check reaches
|
|
191
195
|
|
|
192
|
-
Reading the guide covers the mechanical rules. `jev`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
null handling, immutability,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
Reading the guide covers the mechanical rules. `jev` makes one optional call
|
|
197
|
+
to TypeSafe's decision model about the changed file and, when available, up to
|
|
198
|
+
four directly related source files. It checks guard clauses, fail fast,
|
|
199
|
+
command/query separation, null handling, immutability, repeated validation,
|
|
200
|
+
responsibility, dependency direction, and comments only when the supplied
|
|
201
|
+
files support the question. An answer above the cutoff travels with the
|
|
202
|
+
end-of-run guidelines review instead of interrupting each edit. READMEs, JSON,
|
|
203
|
+
lockfiles, deletion-only edits, and files outside the project are never sent.
|
|
204
|
+
|
|
205
|
+
It never blocks and never enters the system prompt. No conversation or session
|
|
206
|
+
content leaves the machine. Related source excludes ignored, hidden, sensitive-
|
|
207
|
+
named and symlinked paths; a normal source file can still contain a secret.
|
|
201
208
|
The footer counts the calls the session made, and `F10` shows the same count
|
|
202
209
|
beside the key source. Without a key nothing is called, and when Jev runs out of
|
|
203
210
|
quota, rejects the key,
|
|
204
|
-
or stops answering, the extension goes quiet without touching the turn.
|
|
211
|
+
or stops answering, the extension goes quiet without touching the turn. A rate
|
|
212
|
+
limit only pauses it until your next prompt, and Escape cuts a request in
|
|
213
|
+
flight. The key
|
|
205
214
|
lives in `TYPESAFE_API_KEY` or in your agent directory, and `F10` captures it
|
|
206
215
|
without ever writing it to the session.
|
|
207
216
|
|
|
@@ -210,12 +219,12 @@ without ever writing it to the session.
|
|
|
210
219
|
`sideroom_persona` is the agent's voice, and there is exactly one of it. A
|
|
211
220
|
short reminder rides the system prompt on every turn, so it survives
|
|
212
221
|
compaction; the full guide lives in the on-demand `sideroom-persona` skill.
|
|
213
|
-
The voice is direct and dry, free of filler
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
adds block the mutation; flattering
|
|
217
|
-
AI meta-commentary are answered
|
|
218
|
-
no profile selection to store.
|
|
222
|
+
The voice is direct and dry, free of filler. It uses everyday words even when
|
|
223
|
+
you use technical terms. It keeps a technical term only when needed to
|
|
224
|
+
understand or act, and explains it the first time. Emojis and decorative
|
|
225
|
+
symbols in the lines a `write`/`edit` adds block the mutation; flattering
|
|
226
|
+
openers, hedging, automatic apologies, and AI meta-commentary are answered
|
|
227
|
+
with a capped steer. No profiles to switch and no profile selection to store.
|
|
219
228
|
|
|
220
229
|
### Done — done means green
|
|
221
230
|
|
|
@@ -230,11 +239,12 @@ nothing is detectable, the gate does nothing.
|
|
|
230
239
|
|
|
231
240
|
When an implementation settles, `explain` steers the agent to offer a
|
|
232
241
|
walkthrough through `sideroom_ask`: explain the changes only, how to test them
|
|
233
|
-
only, both, or nothing. It fires on `
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
runs first; both
|
|
237
|
-
extension load.
|
|
242
|
+
only, both, or nothing. It fires on `agent_before_settle`, after every retry
|
|
243
|
+
and compaction recovery, so the offer never lands on work that is about to be
|
|
244
|
+
redone, and it stays inside the same run. It waits one boundary so the
|
|
245
|
+
guidelines review runs first; both use the same event, and Pi would otherwise
|
|
246
|
+
order them by extension load. It stays quiet after Escape, after an error, and
|
|
247
|
+
while a message you typed is waiting. Once per turn, only when the turn touched at least five distinct
|
|
238
248
|
files, and only in the TUI, because `sideroom_ask` cannot run anywhere else. No
|
|
239
249
|
tool, no widget, no persisted state.
|
|
240
250
|
|
|
@@ -312,10 +322,12 @@ also fully read the packaged guide matching the target path. Reads with
|
|
|
312
322
|
The extension blocks the mutation until the required reads succeed; unsupported
|
|
313
323
|
languages use the shared table. Do not route around
|
|
314
324
|
the gate through Bash or another file-mutation path. After any successful
|
|
315
|
-
`write`/`edit`, one hidden review steer fires on `
|
|
316
|
-
per turn) asking the agent to re-check every changed file against the
|
|
317
|
-
guide and run the relevant project checks.
|
|
318
|
-
|
|
325
|
+
`write`/`edit`, one hidden review steer fires on `agent_before_settle` (at most
|
|
326
|
+
once per turn) asking the agent to re-check every changed file against the
|
|
327
|
+
loaded guide and run the relevant project checks. It is skipped after Escape,
|
|
328
|
+
after an error, and while a user message is pending. Interactive input sent
|
|
329
|
+
while the agent is idle clears the steer flags; a message typed during a run
|
|
330
|
+
and `before_agent_start` do not, so a steer continuation never re-triggers
|
|
319
331
|
itself.
|
|
320
332
|
|
|
321
333
|
### Monorepo-skills contract
|
|
@@ -347,15 +359,23 @@ itself.
|
|
|
347
359
|
|
|
348
360
|
- Off without a key: no request, and behavior identical to a package without
|
|
349
361
|
this extension.
|
|
350
|
-
- Warn only. A finding
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
362
|
+
- Warn only. A finding goes to the guidelines review on `sideroom:review-note`
|
|
363
|
+
and rides its end-of-run steer, with one follow-up per turn for notes that
|
|
364
|
+
arrive after the review fired; nothing blocks and nothing reaches the system
|
|
365
|
+
prompt.
|
|
366
|
+
- One request per successful `write`/`edit` of a project file in a supported
|
|
367
|
+
language that added lines, with applicable `noul` questions on the complete
|
|
368
|
+
changed file and up to four directly related source files.
|
|
369
|
+
- The official 32k-token state-plus-longest-question and 64k-token request
|
|
370
|
+
limits are observed conservatively using 32,000 and 64,000 UTF-8 bytes.
|
|
371
|
+
Oversized neighbours are omitted whole; if the changed file cannot fit,
|
|
372
|
+
the call is skipped. An answer at or above `0.8` becomes a note.
|
|
373
|
+
- The request follows the run's abort signal; a cancelled request is not a
|
|
374
|
+
failure.
|
|
375
|
+
- Quota (`402`) and a rejected key (`401`, `403`) stop the calls immediately. A
|
|
376
|
+
rate limit (`429`) pauses them until the next idle prompt. Network errors and
|
|
377
|
+
other `4xx`/`5xx` stop after three consecutive failures. A success rearms the
|
|
378
|
+
breaker, and `F10` rearms it by hand.
|
|
359
379
|
- Every request increments a session call counter, shown in the footer and on
|
|
360
380
|
`F10`; it resets on `session_start` and is never persisted.
|
|
361
381
|
- The key comes from `TYPESAFE_API_KEY` or from
|
|
@@ -390,12 +410,13 @@ itself.
|
|
|
390
410
|
|
|
391
411
|
### Explain contract
|
|
392
412
|
|
|
393
|
-
- Trigger: `
|
|
394
|
-
last user prompt, in TUI mode
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
next
|
|
413
|
+
- Trigger: `agent_before_settle` with at least five distinct files mutated
|
|
414
|
+
since the last idle user prompt, in TUI mode, on a completed run with no user
|
|
415
|
+
message pending. File identity follows Pi's path aliases and canonicalizes
|
|
416
|
+
existing paths.
|
|
417
|
+
- One boundary of delay: the guidelines review steers on the same event, and Pi
|
|
418
|
+
runs both handlers in extension load order, so `explain` offers at the next
|
|
419
|
+
boundary.
|
|
399
420
|
- The steer names the intent; the agent writes the question in the user's
|
|
400
421
|
language with four options, one of them a decline, and one recommendation.
|
|
401
422
|
`sideroom_ask` still adds *Out of scope* and the custom answer.
|
package/extensions/ask/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ export const ASK_PROMPT_SNIPPET =
|
|
|
15
15
|
'Ask the user one or more questions with a recommended option, in their language.';
|
|
16
16
|
|
|
17
17
|
export const ASK_PROMPT_GUIDELINES = [
|
|
18
|
-
'
|
|
19
|
-
'
|
|
18
|
+
'Follow the sideroom-grill skill before non-trivial work without a settled spec. One sideroom_ask call is one batch; call again for another round.',
|
|
19
|
+
'Read the repository first: reuse a consistent project fact. An architectural decision (boundaries, dependencies, cost, security, operations, persistence, public contracts, future change difficulty) is never settled silently: never assume it. Follow the sideroom-architecture skill; ask through sideroom_ask with practical consequences and a one-line justification for one recommendation. Name inconsistent patterns in the prompt. Take minor decisions yourself.',
|
|
20
|
+
'Recommend evidence-backed long-term quality: maintainability and growth for known needs over initial speed. State the upfront cost; avoid speculative capacity. If decisive facts or figures are missing, ask for them first through sideroom_ask; recommend gathering evidence, not an unsupported solution.',
|
|
20
21
|
'Write sideroom_ask prompts, tab labels, option labels, and option descriptions in the language the user is speaking; keep ids and option values in English.',
|
|
21
22
|
'sideroom_ask always adds Out of scope and a write-your-own answer; never send them yourself. A single-selection question needs recommendationIndex; a multiple-selection question needs recommendedIndices and never recommendationIndex.',
|
|
22
23
|
'sideroom_ask only works in the interactive TUI; it errors in print, JSON, and RPC modes.',
|
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
isEditToolResult,
|
|
4
4
|
isWriteToolResult,
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import {
|
|
7
|
+
acceptsSettleSteer,
|
|
8
|
+
startsUserTurn,
|
|
9
|
+
withHiddenSteer,
|
|
10
|
+
} from '../shared/settle.ts';
|
|
6
11
|
import {
|
|
7
12
|
applyOfferDecision,
|
|
8
13
|
decideOffer,
|
|
@@ -24,7 +29,7 @@ export function registerExplainGuard(
|
|
|
24
29
|
state: ExplainState,
|
|
25
30
|
): void {
|
|
26
31
|
pi.on('input', (event) => {
|
|
27
|
-
if (event
|
|
32
|
+
if (!startsUserTurn(event)) {
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
30
35
|
resetExplainTurn(state);
|
|
@@ -46,21 +51,18 @@ export function registerExplainGuard(
|
|
|
46
51
|
recordMutation(state, pathKey);
|
|
47
52
|
});
|
|
48
53
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
54
|
+
// agent_before_settle runs after retries and compaction recovery, so the
|
|
55
|
+
// walkthrough is never offered over reworked work, and its continuation
|
|
56
|
+
// stays inside the same run instead of starting a second one.
|
|
57
|
+
pi.on('agent_before_settle', (event, ctx) => {
|
|
58
|
+
if (!acceptsSettleSteer(event)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
52
61
|
const decision = decideOffer(state, ctx.mode);
|
|
53
62
|
applyOfferDecision(state, decision);
|
|
54
63
|
if (decision !== EXPLAIN_DECISION.send) {
|
|
55
64
|
return;
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
customType: EXPLAIN_OFFER_TYPE,
|
|
60
|
-
content: EXPLAIN_OFFER,
|
|
61
|
-
display: false,
|
|
62
|
-
},
|
|
63
|
-
{ triggerTurn: true, deliverAs: 'steer' },
|
|
64
|
-
);
|
|
66
|
+
return withHiddenSteer(event, EXPLAIN_OFFER_TYPE, EXPLAIN_OFFER);
|
|
65
67
|
});
|
|
66
68
|
}
|
|
@@ -54,9 +54,10 @@ export function recordMutation(state: ExplainState, pathKey: string): void {
|
|
|
54
54
|
state.mutatedSinceSettle = true;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// The guidelines review steers at the first settle after a mutation,
|
|
58
|
-
//
|
|
59
|
-
// holds its
|
|
57
|
+
// The guidelines review steers at the first settle boundary after a mutation,
|
|
58
|
+
// and both handlers run in an extension load order Sideroom does not control.
|
|
59
|
+
// Explain holds its steer back one boundary so the review always runs first;
|
|
60
|
+
// the review's continuation is what brings the next boundary.
|
|
60
61
|
export function decideOffer(
|
|
61
62
|
state: ExplainState,
|
|
62
63
|
mode: string,
|
|
@@ -3,36 +3,82 @@ import {
|
|
|
3
3
|
isEditToolResult,
|
|
4
4
|
isWriteToolResult,
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { isReviewNote, REVIEW_NOTE_EVENT } from '../shared/review-note.ts';
|
|
7
|
+
import {
|
|
8
|
+
acceptsSettleSteer,
|
|
9
|
+
startsUserTurn,
|
|
10
|
+
withHiddenSteer,
|
|
11
|
+
} from '../shared/settle.ts';
|
|
6
12
|
|
|
7
13
|
export const REVIEW_STEER_TYPE = 'sideroom-guidelines-review';
|
|
8
14
|
|
|
9
15
|
export const REVIEW_STEER =
|
|
10
16
|
'Review every file this run changed against the loaded Sideroom language guide: fix any mechanical, structural, or architectural rule violations in those files, then run the relevant formatter, linter, type checks, and tests. If the guide is no longer in context, re-read it in full first.';
|
|
11
17
|
|
|
18
|
+
export const REVIEW_NOTES_STEER =
|
|
19
|
+
'Files changed during the review drew the semantic notes below. Verify each against the loaded Sideroom language guide, fix only real violations, then rerun the relevant checks.';
|
|
20
|
+
|
|
12
21
|
export interface ReviewState {
|
|
13
22
|
mutated: boolean;
|
|
14
23
|
steeredThisTurn: boolean;
|
|
24
|
+
notesSteeredThisTurn: boolean;
|
|
25
|
+
notes: string[];
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
export function createReviewState(): ReviewState {
|
|
18
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
mutated: false,
|
|
31
|
+
steeredThisTurn: false,
|
|
32
|
+
notesSteeredThisTurn: false,
|
|
33
|
+
notes: [],
|
|
34
|
+
};
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
export function resetReviewTurn(state: ReviewState): void {
|
|
22
38
|
state.mutated = false;
|
|
23
39
|
state.steeredThisTurn = false;
|
|
40
|
+
state.notesSteeredThisTurn = false;
|
|
41
|
+
state.notes = [];
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
export function shouldReview(state: ReviewState): boolean {
|
|
27
45
|
return state.mutated && !state.steeredThisTurn;
|
|
28
46
|
}
|
|
29
47
|
|
|
48
|
+
// Notes that arrive after the review fired get one follow-up per turn, so a
|
|
49
|
+
// fix that draws new notes cannot keep the run going.
|
|
50
|
+
export function pendingReviewSteer(state: ReviewState): string | undefined {
|
|
51
|
+
if (shouldReview(state)) {
|
|
52
|
+
return withNotes(REVIEW_STEER, state.notes);
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
state.steeredThisTurn &&
|
|
56
|
+
!state.notesSteeredThisTurn &&
|
|
57
|
+
state.notes.length > 0
|
|
58
|
+
) {
|
|
59
|
+
return withNotes(REVIEW_NOTES_STEER, state.notes);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function markReviewSteered(state: ReviewState): void {
|
|
65
|
+
if (state.steeredThisTurn) {
|
|
66
|
+
state.notesSteeredThisTurn = true;
|
|
67
|
+
}
|
|
68
|
+
state.steeredThisTurn = true;
|
|
69
|
+
state.notes = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function withNotes(lead: string, notes: readonly string[]): string {
|
|
73
|
+
return [lead, ...notes].join('\n\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
30
76
|
export function registerReviewGuard(
|
|
31
77
|
pi: ExtensionAPI,
|
|
32
78
|
state: ReviewState,
|
|
33
79
|
): void {
|
|
34
80
|
pi.on('input', (event) => {
|
|
35
|
-
if (event
|
|
81
|
+
if (!startsUserTurn(event)) {
|
|
36
82
|
return;
|
|
37
83
|
}
|
|
38
84
|
resetReviewTurn(state);
|
|
@@ -48,23 +94,22 @@ export function registerReviewGuard(
|
|
|
48
94
|
state.mutated = true;
|
|
49
95
|
});
|
|
50
96
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
pi.events.on(REVIEW_NOTE_EVENT, (payload) => {
|
|
98
|
+
if (isReviewNote(payload)) {
|
|
99
|
+
state.notes.push(payload.text);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// agent_before_settle runs after retries and compaction recovery, and its
|
|
104
|
+
// continuation stays inside the same run. agent_settled is notification-only:
|
|
105
|
+
// a steer sent there starts a second run after the user already saw the
|
|
106
|
+
// agent stop, and it fires even when the user pressed Escape.
|
|
107
|
+
pi.on('agent_before_settle', (event) => {
|
|
108
|
+
const steer = pendingReviewSteer(state);
|
|
109
|
+
if (steer === undefined || !acceptsSettleSteer(event)) {
|
|
58
110
|
return;
|
|
59
111
|
}
|
|
60
|
-
state
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
customType: REVIEW_STEER_TYPE,
|
|
64
|
-
content: REVIEW_STEER,
|
|
65
|
-
display: false,
|
|
66
|
-
},
|
|
67
|
-
{ triggerTurn: true, deliverAs: 'steer' },
|
|
68
|
-
);
|
|
112
|
+
markReviewSteered(state);
|
|
113
|
+
return withHiddenSteer(event, REVIEW_STEER_TYPE, steer);
|
|
69
114
|
});
|
|
70
115
|
}
|
package/extensions/jev/client.ts
CHANGED
|
@@ -12,7 +12,17 @@ export const JEV_REQUEST_TIMEOUT_MS = 3_000;
|
|
|
12
12
|
export const JEV_FAILURE = {
|
|
13
13
|
quota: 'quota',
|
|
14
14
|
auth: 'auth',
|
|
15
|
+
rateLimited: 'rateLimited',
|
|
15
16
|
transient: 'transient',
|
|
17
|
+
cancelled: 'cancelled',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const HTTP_STATUS = {
|
|
21
|
+
unauthorized: 401,
|
|
22
|
+
paymentRequired: 402,
|
|
23
|
+
forbidden: 403,
|
|
24
|
+
tooManyRequests: 429,
|
|
25
|
+
firstClientError: 400,
|
|
16
26
|
} as const;
|
|
17
27
|
|
|
18
28
|
export type JevFailure = (typeof JEV_FAILURE)[keyof typeof JEV_FAILURE];
|
|
@@ -20,6 +30,7 @@ export type JevFailure = (typeof JEV_FAILURE)[keyof typeof JEV_FAILURE];
|
|
|
20
30
|
export interface JevHttpRequest {
|
|
21
31
|
readonly apiKey: string;
|
|
22
32
|
readonly body: string;
|
|
33
|
+
readonly signal: AbortSignal | undefined;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
export interface JevHttpResponse {
|
|
@@ -50,20 +61,36 @@ export function createFetchTransport(
|
|
|
50
61
|
'Content-Type': 'application/json',
|
|
51
62
|
},
|
|
52
63
|
body: request.body,
|
|
53
|
-
signal:
|
|
64
|
+
signal: requestSignal(timeoutMs, request.signal),
|
|
54
65
|
});
|
|
55
66
|
return { status: response.status, text: await response.text() };
|
|
56
67
|
};
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
// Pi waits for every tool_result handler before the result reaches the model,
|
|
71
|
+
// so the user's Escape has to cut the request, not only the timeout.
|
|
72
|
+
function requestSignal(
|
|
73
|
+
timeoutMs: number,
|
|
74
|
+
runSignal: AbortSignal | undefined,
|
|
75
|
+
): AbortSignal {
|
|
76
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
77
|
+
if (runSignal === undefined) {
|
|
78
|
+
return timeout;
|
|
79
|
+
}
|
|
80
|
+
return AbortSignal.any([timeout, runSignal]);
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export function classifyStatus(status: number): JevFailure | undefined {
|
|
60
|
-
if (status ===
|
|
84
|
+
if (status === HTTP_STATUS.unauthorized || status === HTTP_STATUS.forbidden) {
|
|
61
85
|
return JEV_FAILURE.auth;
|
|
62
86
|
}
|
|
63
|
-
if (status ===
|
|
87
|
+
if (status === HTTP_STATUS.paymentRequired) {
|
|
64
88
|
return JEV_FAILURE.quota;
|
|
65
89
|
}
|
|
66
|
-
if (status
|
|
90
|
+
if (status === HTTP_STATUS.tooManyRequests) {
|
|
91
|
+
return JEV_FAILURE.rateLimited;
|
|
92
|
+
}
|
|
93
|
+
if (status >= HTTP_STATUS.firstClientError) {
|
|
67
94
|
return JEV_FAILURE.transient;
|
|
68
95
|
}
|
|
69
96
|
return undefined;
|
|
@@ -76,16 +103,21 @@ export async function askJev(
|
|
|
76
103
|
transport: JevTransport,
|
|
77
104
|
apiKey: string,
|
|
78
105
|
file: JevFileState,
|
|
106
|
+
signal: AbortSignal | undefined,
|
|
79
107
|
): Promise<JevAttempt> {
|
|
80
|
-
const
|
|
108
|
+
const requestBody = buildRequestBody(file);
|
|
109
|
+
const body = JSON.stringify(requestBody);
|
|
81
110
|
let response: JevHttpResponse;
|
|
82
111
|
try {
|
|
83
|
-
response = await transport({ apiKey, body });
|
|
112
|
+
response = await transport({ apiKey, body, signal });
|
|
84
113
|
} catch {
|
|
85
114
|
return {
|
|
86
115
|
findings: undefined,
|
|
87
116
|
model: undefined,
|
|
88
|
-
failure:
|
|
117
|
+
failure:
|
|
118
|
+
signal?.aborted === true
|
|
119
|
+
? JEV_FAILURE.cancelled
|
|
120
|
+
: JEV_FAILURE.transient,
|
|
89
121
|
};
|
|
90
122
|
}
|
|
91
123
|
|
|
@@ -99,7 +131,10 @@ export async function askJev(
|
|
|
99
131
|
return { findings: [], model: undefined, failure: undefined };
|
|
100
132
|
}
|
|
101
133
|
return {
|
|
102
|
-
findings: readFindings(
|
|
134
|
+
findings: readFindings(
|
|
135
|
+
envelope,
|
|
136
|
+
new Set(Object.keys(requestBody.questions)),
|
|
137
|
+
),
|
|
103
138
|
model: envelope.model,
|
|
104
139
|
failure: undefined,
|
|
105
140
|
};
|