pi-harness-runtime 1.1.447 → 1.1.450
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/AGENTS.md +82 -9
- package/CHANGELOG.md +45 -0
- package/README.md +74 -0
- package/harness/key-file-audit.ts +252 -42
- package/harness/startup-report.ts +32 -7
- package/index.ts +142 -22
- package/package.json +2 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/dist/auto-continue.d.ts +39 -1
- package/packages/jev-judge/dist/auto-continue.d.ts.map +1 -1
- package/packages/jev-judge/dist/auto-continue.js +91 -3
- package/packages/jev-judge/dist/auto-continue.js.map +1 -1
- package/packages/jev-judge/dist/autonomy-instruction.d.ts +52 -0
- package/packages/jev-judge/dist/autonomy-instruction.d.ts.map +1 -0
- package/packages/jev-judge/dist/autonomy-instruction.js +92 -0
- package/packages/jev-judge/dist/autonomy-instruction.js.map +1 -0
- package/packages/jev-judge/dist/index.d.ts +3 -1
- package/packages/jev-judge/dist/index.d.ts.map +1 -1
- package/packages/jev-judge/dist/index.js +2 -0
- package/packages/jev-judge/dist/index.js.map +1 -1
- package/packages/jev-judge/dist/question-risk.d.ts +8 -0
- package/packages/jev-judge/dist/question-risk.d.ts.map +1 -1
- package/packages/jev-judge/dist/question-risk.js +7 -0
- package/packages/jev-judge/dist/question-risk.js.map +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/auto-continue.ts +134 -5
- package/packages/jev-judge/src/autonomy-instruction.ts +140 -0
- package/packages/jev-judge/src/index.ts +16 -1
- package/packages/jev-judge/src/question-risk.ts +16 -0
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/notification/dist/pending-question.d.ts +114 -3
- package/packages/notification/dist/pending-question.d.ts.map +1 -1
- package/packages/notification/dist/pending-question.js +277 -12
- package/packages/notification/dist/pending-question.js.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/pending-question.ts +389 -11
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -13,9 +13,30 @@ import { logWarn } from "./file-logger.js";
|
|
|
13
13
|
export type QuestionKind =
|
|
14
14
|
| "routine" // "Should I continue?"
|
|
15
15
|
| "approval" // "Want me to implement X?"
|
|
16
|
+
| "choice" // "Do you want X, or is Y intended?"
|
|
16
17
|
| "clarification" // "Do you mean X or Y?"
|
|
17
18
|
| "explicit"; // Explicit approval required
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The typed ask of a detected question.
|
|
22
|
+
*
|
|
23
|
+
* An ask is not the same thing as a question: a question is a sentence, an
|
|
24
|
+
* ask is what the agent wants. The distinction matters after the timeout:
|
|
25
|
+
* an ask of type "choice" can be decided autonomously (the decision names
|
|
26
|
+
* the chosen option), while an ask of type "clarification" or
|
|
27
|
+
* "explicit_authorization" waits for a human.
|
|
28
|
+
*
|
|
29
|
+
* A question the detector cannot interpret is NOT a routine ask: an
|
|
30
|
+
* unrecognized question is a clarification, because the harness does not
|
|
31
|
+
* know what the agent is waiting for and must not guess.
|
|
32
|
+
*/
|
|
33
|
+
export type AskType =
|
|
34
|
+
| "approval"
|
|
35
|
+
| "choice"
|
|
36
|
+
| "clarification"
|
|
37
|
+
| "explicit_authorization"
|
|
38
|
+
| "routine";
|
|
39
|
+
|
|
19
40
|
/**
|
|
20
41
|
* Question states
|
|
21
42
|
*/
|
|
@@ -69,6 +90,18 @@ export interface PendingQuestion {
|
|
|
69
90
|
questionText: string;
|
|
70
91
|
/** Question kind */
|
|
71
92
|
kind: QuestionKind;
|
|
93
|
+
/** The typed ask of the question. */
|
|
94
|
+
askType?: AskType;
|
|
95
|
+
/** The concrete alternatives of a choice question. */
|
|
96
|
+
alternatives?: string[];
|
|
97
|
+
/** The complete assistant message the question was detected in. */
|
|
98
|
+
contextText?: string;
|
|
99
|
+
/** The option Jev selected for a choice question. */
|
|
100
|
+
selectedOption?: string;
|
|
101
|
+
/** The scope Jev chose for the selected option. */
|
|
102
|
+
scope?: DecisionScope;
|
|
103
|
+
/** The safe reason code of the last state transition. */
|
|
104
|
+
closedReason?: string;
|
|
72
105
|
/** Does the proposed action need an explicit human authorization? */
|
|
73
106
|
requiresExplicitAuthorization?: boolean;
|
|
74
107
|
/** Can the proposed action destroy or damage something? */
|
|
@@ -118,6 +151,205 @@ export interface DetectionResult {
|
|
|
118
151
|
questionText?: string;
|
|
119
152
|
kind?: QuestionKind;
|
|
120
153
|
isRoutine?: boolean;
|
|
154
|
+
/** The typed ask of the detected question. */
|
|
155
|
+
askType?: AskType;
|
|
156
|
+
/** The concrete alternatives of a choice question. */
|
|
157
|
+
alternatives?: string[];
|
|
158
|
+
/** The complete assistant message the question was detected in. */
|
|
159
|
+
contextText?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The scope of an autonomous decision.
|
|
164
|
+
*
|
|
165
|
+
* A large or expensive job is not done in one step: the decision may
|
|
166
|
+
* limit itself to a canary that is verified before the work scales up.
|
|
167
|
+
*/
|
|
168
|
+
export type DecisionScope = "full_scope" | "canary_first" | "none";
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The structured resolution of an autonomous decision.
|
|
172
|
+
*
|
|
173
|
+
* It is the answer to the question the agent asked: the selected option,
|
|
174
|
+
* the scope and the reasoning. It is never a human answer.
|
|
175
|
+
*/
|
|
176
|
+
export interface JevResolution {
|
|
177
|
+
/** The option Jev selected for a choice question. */
|
|
178
|
+
selectedOption?: string;
|
|
179
|
+
/** The scope Jev chose. */
|
|
180
|
+
scope?: DecisionScope;
|
|
181
|
+
/** The reasoning of the decision. */
|
|
182
|
+
reasoning?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The wording that asks for an explicit human authorization.
|
|
187
|
+
*
|
|
188
|
+
* The agent states the requirement itself ("I need your OK before ..."). It
|
|
189
|
+
* is part of the context of the question, not of the question sentence, so
|
|
190
|
+
* it is detected on the whole message.
|
|
191
|
+
*/
|
|
192
|
+
const EXPLICIT_AUTHORIZATION_PATTERNS: RegExp[] = [
|
|
193
|
+
/\b(?:need|needs|needing|require|requires|requiring|want|wants|waiting for|wait for)\s+(?:your|the)\s+(?:explicit\s+)?(?:ok|okay|approval|approbation|authorization|authorisation|permission|consent|confirmation|green light|sign[- ]?off|go[- ]?ahead)\b/i,
|
|
194
|
+
/\bexplicit\s+(?:approval|authorization|authorisation|permission)\b/i,
|
|
195
|
+
/\b(?:authorize|authorise)\s+me\b/i,
|
|
196
|
+
/\bonly\s+(?:with|after)\s+your\b/i,
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Does the message ask for an explicit human authorization?
|
|
201
|
+
*
|
|
202
|
+
* An ask of this kind is never decided autonomously: only the human may
|
|
203
|
+
* authorize it.
|
|
204
|
+
*/
|
|
205
|
+
export function hasExplicitAuthorizationWording(text: string): boolean {
|
|
206
|
+
return EXPLICIT_AUTHORIZATION_PATTERNS.some((pattern) => pattern.test(text));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The typed ask of a question kind.
|
|
211
|
+
*/
|
|
212
|
+
export function askTypeForKind(kind: QuestionKind): AskType {
|
|
213
|
+
switch (kind) {
|
|
214
|
+
case "approval":
|
|
215
|
+
return "approval";
|
|
216
|
+
case "choice":
|
|
217
|
+
return "choice";
|
|
218
|
+
case "clarification":
|
|
219
|
+
return "clarification";
|
|
220
|
+
case "explicit":
|
|
221
|
+
return "explicit_authorization";
|
|
222
|
+
case "routine":
|
|
223
|
+
return "routine";
|
|
224
|
+
default:
|
|
225
|
+
// An unknown kind is a clarification: the harness does not know
|
|
226
|
+
// what the agent is waiting for, so it waits for a human.
|
|
227
|
+
return "clarification";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The typed ask of a detected question.
|
|
233
|
+
*
|
|
234
|
+
* A choice keeps its type: the agent offers concrete alternatives, and a
|
|
235
|
+
* decision may pick one of them. Every other ask that the message
|
|
236
|
+
* explicitly reserves for the human is an explicit authorization.
|
|
237
|
+
*/
|
|
238
|
+
export function computeAskType(
|
|
239
|
+
kind: QuestionKind,
|
|
240
|
+
explicitAuthorization: boolean,
|
|
241
|
+
): AskType {
|
|
242
|
+
if (kind === "choice") return "choice";
|
|
243
|
+
if (explicitAuthorization) return "explicit_authorization";
|
|
244
|
+
return askTypeForKind(kind);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The stem of a question: the part that carries no alternative.
|
|
249
|
+
*/
|
|
250
|
+
const QUESTION_STEM =
|
|
251
|
+
/^(?:do you want(?: me to)?|do you prefer|would you like me to|should i|should we|shall i|shall we|may i|can i|could i|is it|are you|which|what|do)\s+/i;
|
|
252
|
+
|
|
253
|
+
/** The auxiliary verb of the second alternative. */
|
|
254
|
+
const ALTERNATIVE_AUXILIARY =
|
|
255
|
+
/^(?:is|are|was|were|should|would|do|does|did|can|could|shall|will|to)\s+/i;
|
|
256
|
+
|
|
257
|
+
/** The intent tail of an alternative ("... is intended"). */
|
|
258
|
+
const ALTERNATIVE_INTENT =
|
|
259
|
+
/\s+(intended|expected|needed|required|desired|meant|correct|right|preferred)$/i;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The longest question text that is parsed.
|
|
263
|
+
*
|
|
264
|
+
* A question is a sentence. The cap keeps the work of the parser
|
|
265
|
+
* bounded: the alternatives of a question are not longer than a line,
|
|
266
|
+
* and a bounded input cannot make a slow pattern slow.
|
|
267
|
+
*/
|
|
268
|
+
const MAX_QUESTION_LENGTH = 500;
|
|
269
|
+
|
|
270
|
+
/** Is the character whitespace? */
|
|
271
|
+
function isSpace(character: string | undefined): boolean {
|
|
272
|
+
return typeof character === "string" && /\s/.test(character);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Is the character the end of a sentence? */
|
|
276
|
+
function isSentenceEnd(character: string | undefined): boolean {
|
|
277
|
+
return character === "?" || character === "!" || character === ".";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The question without the punctuation that ends the sentence.
|
|
282
|
+
*
|
|
283
|
+
* The punctuation is removed by a scan and not by a pattern: a pattern
|
|
284
|
+
* with two unbounded repetitions next to each other takes quadratic time
|
|
285
|
+
* on a long run of the same character.
|
|
286
|
+
*/
|
|
287
|
+
function withoutSentenceEnd(text: string): string {
|
|
288
|
+
let end = text.length;
|
|
289
|
+
while (end > 0 && isSentenceEnd(text[end - 1])) end -= 1;
|
|
290
|
+
return text.slice(0, end).trimEnd();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The index where the "or" of two alternatives starts.
|
|
295
|
+
*
|
|
296
|
+
* The separator is the word "or" with whitespace around it. A comma in
|
|
297
|
+
* front of it belongs to the separator. The search is a scan over the
|
|
298
|
+
* string: a pattern with two unbounded repetitions can take quadratic
|
|
299
|
+
* time, and a question is not a fixed length.
|
|
300
|
+
*
|
|
301
|
+
* @returns the boundaries of the separator, or null when there is none.
|
|
302
|
+
*/
|
|
303
|
+
function findOrSeparator(text: string): { leftEnd: number; orIndex: number } | null {
|
|
304
|
+
const lower = text.toLowerCase();
|
|
305
|
+
for (let index = 0; index + 1 < text.length; index += 1) {
|
|
306
|
+
if (lower[index] !== "o" || lower[index + 1] !== "r") continue;
|
|
307
|
+
if (!isSpace(text[index - 1]) || !isSpace(text[index + 2])) continue;
|
|
308
|
+
// The whitespace in front of the word, and the comma that may
|
|
309
|
+
// come with it.
|
|
310
|
+
let start = index;
|
|
311
|
+
while (start > 0 && isSpace(text[start - 1])) start -= 1;
|
|
312
|
+
const leftEnd = text[start - 1] === "," ? start - 1 : start;
|
|
313
|
+
return { leftEnd, orIndex: index };
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Extract the alternatives of a choice question.
|
|
320
|
+
*
|
|
321
|
+
* The result is what the decision has to choose between, so it must be
|
|
322
|
+
* concrete: "the current staging state is intended" is an alternative,
|
|
323
|
+
* "is the current staging state intended?" is not. An empty or unusable
|
|
324
|
+
* half means the question is not a choice after all.
|
|
325
|
+
*/
|
|
326
|
+
export function extractAlternatives(question: string): string[] {
|
|
327
|
+
const text = withoutSentenceEnd(
|
|
328
|
+
question.trim().slice(0, MAX_QUESTION_LENGTH),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const separator = findOrSeparator(text);
|
|
332
|
+
if (!separator) return [];
|
|
333
|
+
|
|
334
|
+
// The left half ends before the separator, the right half after the
|
|
335
|
+
// word "or".
|
|
336
|
+
const left = text
|
|
337
|
+
.slice(0, separator.leftEnd)
|
|
338
|
+
.replace(QUESTION_STEM, "")
|
|
339
|
+
.replace(/[\s,]+$/, "")
|
|
340
|
+
.trim();
|
|
341
|
+
const rightRaw = text.slice(separator.orIndex + 2).trim();
|
|
342
|
+
const right = rightRaw.replace(ALTERNATIVE_AUXILIARY, "").trim();
|
|
343
|
+
|
|
344
|
+
if (!left || !right) return [];
|
|
345
|
+
|
|
346
|
+
// "the current staging state intended" -> "the current staging state is intended"
|
|
347
|
+
const intent = right.match(ALTERNATIVE_INTENT);
|
|
348
|
+
const normalizedRight = intent
|
|
349
|
+
? `${right.slice(0, intent.index).trimEnd()} is ${intent[1]}`
|
|
350
|
+
: right;
|
|
351
|
+
|
|
352
|
+
return [left, normalizedRight];
|
|
121
353
|
}
|
|
122
354
|
|
|
123
355
|
/**
|
|
@@ -176,13 +408,26 @@ export class PendingQuestionController {
|
|
|
176
408
|
|
|
177
409
|
/**
|
|
178
410
|
* Detect if text contains a question requiring response
|
|
411
|
+
*
|
|
412
|
+
* The complete assistant message is kept as the context of the question:
|
|
413
|
+
* the sentence that carries the question often leaves out what the agent
|
|
414
|
+
* is really waiting for ("I need your OK before ..."). The risk facts and
|
|
415
|
+
* the decision are derived from the whole message, not from the sentence
|
|
416
|
+
* alone.
|
|
179
417
|
*/
|
|
180
|
-
detectQuestion(text: string,
|
|
418
|
+
detectQuestion(text: string, options?: {
|
|
181
419
|
sessionId?: string;
|
|
182
420
|
workspacePath?: string;
|
|
183
421
|
jobId?: string;
|
|
422
|
+
/** The complete assistant message the question was detected in. */
|
|
423
|
+
contextText?: string;
|
|
184
424
|
}
|
|
185
425
|
): DetectionResult {
|
|
426
|
+
const contextText = options?.contextText ?? text;
|
|
427
|
+
// The message decides whether the agent reserves the decision for the
|
|
428
|
+
// human: "I need your OK before ..." is part of the context and not
|
|
429
|
+
// of the question sentence.
|
|
430
|
+
const explicitAuthorization = hasExplicitAuthorizationWording(contextText);
|
|
186
431
|
// Find all question positions and extract each question sentence
|
|
187
432
|
const questionPositions: Array<{ start: number; end: number; sentence: string }> = [];
|
|
188
433
|
let searchFrom = 0;
|
|
@@ -231,30 +476,39 @@ export class PendingQuestionController {
|
|
|
231
476
|
const isRoutine = this.isRoutineContinuation(q.sentence);
|
|
232
477
|
const kind = this.determineKind(q.sentence, isRoutine);
|
|
233
478
|
|
|
234
|
-
// Return first approval or clarification question found
|
|
235
|
-
if (kind === "approval" || kind === "clarification") {
|
|
479
|
+
// Return first approval, choice or clarification question found
|
|
480
|
+
if (kind === "approval" || kind === "choice" || kind === "clarification") {
|
|
236
481
|
return {
|
|
237
482
|
detected: true,
|
|
238
483
|
questionText: q.sentence,
|
|
239
484
|
kind,
|
|
240
485
|
isRoutine,
|
|
486
|
+
askType: computeAskType(kind, explicitAuthorization),
|
|
487
|
+
alternatives: kind === "choice" ? extractAlternatives(q.sentence) : undefined,
|
|
488
|
+
contextText,
|
|
241
489
|
};
|
|
242
490
|
}
|
|
243
491
|
}
|
|
244
492
|
|
|
245
|
-
// If no approval/clarification found, return first
|
|
493
|
+
// If no approval/choice/clarification was found, return the first
|
|
494
|
+
// non-quoted question. An unrecognized question is a clarification:
|
|
495
|
+
// the harness does not know what the agent is waiting for.
|
|
246
496
|
for (const q of questionPositions) {
|
|
247
497
|
if (this.isQuotedOrCode(text, q.sentence)) {
|
|
248
498
|
continue;
|
|
249
499
|
}
|
|
250
500
|
|
|
251
501
|
const isRoutine = this.isRoutineContinuation(q.sentence);
|
|
502
|
+
const kind = this.determineKind(q.sentence, isRoutine);
|
|
252
503
|
|
|
253
504
|
return {
|
|
254
505
|
detected: true,
|
|
255
506
|
questionText: q.sentence,
|
|
256
|
-
kind
|
|
507
|
+
kind,
|
|
257
508
|
isRoutine,
|
|
509
|
+
askType: computeAskType(kind, explicitAuthorization),
|
|
510
|
+
alternatives: kind === "choice" ? extractAlternatives(q.sentence) : undefined,
|
|
511
|
+
contextText,
|
|
258
512
|
};
|
|
259
513
|
}
|
|
260
514
|
|
|
@@ -305,6 +559,11 @@ export class PendingQuestionController {
|
|
|
305
559
|
questionText: text.trim(),
|
|
306
560
|
kind: "approval",
|
|
307
561
|
isRoutine: false,
|
|
562
|
+
askType: computeAskType(
|
|
563
|
+
"approval",
|
|
564
|
+
hasExplicitAuthorizationWording(text),
|
|
565
|
+
),
|
|
566
|
+
contextText: text,
|
|
308
567
|
};
|
|
309
568
|
}
|
|
310
569
|
}
|
|
@@ -386,10 +645,8 @@ export class PendingQuestionController {
|
|
|
386
645
|
/\bwant me to\b/i,
|
|
387
646
|
/\bwould you like me to\b/i,
|
|
388
647
|
/\bshall i\b/i,
|
|
389
|
-
/\bshould i
|
|
390
|
-
/\bshould
|
|
391
|
-
/\bshould we add\b/i,
|
|
392
|
-
/\bshould we implement\b/i,
|
|
648
|
+
/\bshould i\b/i,
|
|
649
|
+
/\bshould we\b/i,
|
|
393
650
|
/\bdo you want me to\b/i,
|
|
394
651
|
/\bcan i (start|proceed|add|implement)\b/i,
|
|
395
652
|
];
|
|
@@ -400,7 +657,25 @@ export class PendingQuestionController {
|
|
|
400
657
|
}
|
|
401
658
|
}
|
|
402
659
|
|
|
403
|
-
|
|
660
|
+
// A choice names concrete alternatives: "do you want X, or is Y
|
|
661
|
+
// intended?". The decision may pick one of them, but only when it
|
|
662
|
+
// names the picked option.
|
|
663
|
+
const choicePatterns = [
|
|
664
|
+
/,\s*or\s+(?:is|are|was|were|should|would|do|does|did|can|could|shall|will|to)\b/i,
|
|
665
|
+
/\beither\b[^?]*\bor\b/i,
|
|
666
|
+
/\b(?:do you want|do you prefer|would you like|should i|shall i|can i|could i|which)\b[^?]*\bor\b/i,
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
for (const pattern of choicePatterns) {
|
|
670
|
+
if (pattern.test(lower)) {
|
|
671
|
+
return "choice";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// An unrecognized question is not a routine continuation: the harness
|
|
676
|
+
// does not know what the agent is waiting for, so it asks a human
|
|
677
|
+
// instead of continuing on a guess.
|
|
678
|
+
return "clarification";
|
|
404
679
|
}
|
|
405
680
|
|
|
406
681
|
/**
|
|
@@ -447,6 +722,12 @@ export class PendingQuestionController {
|
|
|
447
722
|
workspacePath: string;
|
|
448
723
|
jobId?: string;
|
|
449
724
|
kind: QuestionKind;
|
|
725
|
+
/** The typed ask of the question. */
|
|
726
|
+
askType?: AskType;
|
|
727
|
+
/** The concrete alternatives of a choice question. */
|
|
728
|
+
alternatives?: string[];
|
|
729
|
+
/** The complete assistant message the question was detected in. */
|
|
730
|
+
contextText?: string;
|
|
450
731
|
/** The deterministic risk facts of the proposed action. */
|
|
451
732
|
risk?: {
|
|
452
733
|
requiresExplicitAuthorization?: boolean;
|
|
@@ -465,6 +746,9 @@ export class PendingQuestionController {
|
|
|
465
746
|
jobId: params.jobId,
|
|
466
747
|
questionText: params.text,
|
|
467
748
|
kind: params.kind,
|
|
749
|
+
askType: params.askType ?? askTypeForKind(params.kind),
|
|
750
|
+
alternatives: params.alternatives,
|
|
751
|
+
contextText: params.contextText,
|
|
468
752
|
requiresExplicitAuthorization: params.risk?.requiresExplicitAuthorization,
|
|
469
753
|
destructive: params.risk?.destructive,
|
|
470
754
|
reversible: params.risk?.reversible,
|
|
@@ -554,6 +838,18 @@ export class PendingQuestionController {
|
|
|
554
838
|
return false;
|
|
555
839
|
}
|
|
556
840
|
|
|
841
|
+
// A question that was expired or cancelled is closed: a late click
|
|
842
|
+
// must not resurrect it and must not report an answer that never
|
|
843
|
+
// happened.
|
|
844
|
+
if (
|
|
845
|
+
this.currentQuestion.state === "expired" ||
|
|
846
|
+
this.currentQuestion.state === "cancelled"
|
|
847
|
+
) {
|
|
848
|
+
logWarn("[PendingQuestion]",
|
|
849
|
+
`Question is ${this.currentQuestion.state} (${this.currentQuestion.closedReason ?? "no reason"}) - late callback ignored`);
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
|
|
557
853
|
this.currentQuestion.state = "resolved";
|
|
558
854
|
this.currentQuestion.resolution = params.resolution;
|
|
559
855
|
this.currentQuestion.resolvedBy = params.by;
|
|
@@ -595,6 +891,7 @@ export class PendingQuestionController {
|
|
|
595
891
|
requestId: string,
|
|
596
892
|
outcome: JevOutcome,
|
|
597
893
|
reason?: string,
|
|
894
|
+
resolution?: JevResolution,
|
|
598
895
|
): boolean {
|
|
599
896
|
if (!this.currentQuestion) {
|
|
600
897
|
logWarn("[PendingQuestion]", "No question to resolve - no pending question");
|
|
@@ -613,27 +910,108 @@ export class PendingQuestionController {
|
|
|
613
910
|
return false;
|
|
614
911
|
}
|
|
615
912
|
|
|
913
|
+
if (
|
|
914
|
+
this.currentQuestion.state === "expired" ||
|
|
915
|
+
this.currentQuestion.state === "cancelled"
|
|
916
|
+
) {
|
|
917
|
+
logWarn("[PendingQuestion]",
|
|
918
|
+
`Question is ${this.currentQuestion.state} - jev decision ignored`);
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
616
922
|
this.currentQuestion.state = "resolved";
|
|
617
923
|
this.currentQuestion.resolvedBy = "jev";
|
|
618
924
|
this.currentQuestion.jevOutcome = outcome;
|
|
619
925
|
this.currentQuestion.jevReason = reason;
|
|
620
926
|
this.currentQuestion.resolvedAt = new Date();
|
|
927
|
+
// The resolution is the answer to the question: the selected option and
|
|
928
|
+
// the scope travel with the request, so the agent can be told which
|
|
929
|
+
// option was chosen.
|
|
930
|
+
if (resolution?.selectedOption) {
|
|
931
|
+
this.currentQuestion.selectedOption = resolution.selectedOption;
|
|
932
|
+
}
|
|
933
|
+
if (resolution?.scope) {
|
|
934
|
+
this.currentQuestion.scope = resolution.scope;
|
|
935
|
+
}
|
|
621
936
|
|
|
622
937
|
logWarn("[PendingQuestion]",
|
|
623
938
|
`Closed question ${this.currentQuestion.requestId} with jev outcome ${outcome}`);
|
|
624
939
|
|
|
625
940
|
if (this.jevDecisionCallback) {
|
|
941
|
+
const metadata: Record<string, unknown> = { outcome };
|
|
942
|
+
if (resolution?.selectedOption) {
|
|
943
|
+
metadata["selectedOption"] = resolution.selectedOption;
|
|
944
|
+
}
|
|
945
|
+
if (resolution?.scope) {
|
|
946
|
+
metadata["scope"] = resolution.scope;
|
|
947
|
+
}
|
|
626
948
|
this.jevDecisionCallback(this.currentQuestion, {
|
|
627
949
|
action: outcome === "reject" ? "stop" : outcome === "wait" ? "wait" : "continue",
|
|
628
950
|
reason: reason ?? `jev:${outcome}`,
|
|
629
951
|
eligible: true,
|
|
630
|
-
metadata
|
|
952
|
+
metadata,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Expire the pending question when the decision deadline is reached.
|
|
961
|
+
*
|
|
962
|
+
* The deadline is a terminal path: the question is closed with the safe
|
|
963
|
+
* reason code, the buttons of that request become stale, and the wait
|
|
964
|
+
* state is cleared. A question that is left open here looks like a
|
|
965
|
+
* request that is still waiting - forever.
|
|
966
|
+
*
|
|
967
|
+
* @returns true if the question was expired, false when it was already
|
|
968
|
+
* answered by a human or closed by a decision.
|
|
969
|
+
*/
|
|
970
|
+
expireByTimeout(requestId: string, reason: string = "deadline-expired"): boolean {
|
|
971
|
+
if (!this.currentQuestion) {
|
|
972
|
+
logWarn("[PendingQuestion]", "No question to expire - no pending question");
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (this.currentQuestion.requestId !== requestId) {
|
|
977
|
+
logWarn("[PendingQuestion]",
|
|
978
|
+
`Expiry for an unknown request: ${requestId} (current=${this.currentQuestion.requestId})`);
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (this.currentQuestion.state !== "pending" && this.currentQuestion.state !== "evaluating") {
|
|
983
|
+
logWarn("[PendingQuestion]",
|
|
984
|
+
`Question is ${this.currentQuestion.state} - expiry ignored`);
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
this.currentQuestion.state = "expired";
|
|
989
|
+
this.currentQuestion.resolvedBy = "timeout";
|
|
990
|
+
this.currentQuestion.resolvedAt = new Date();
|
|
991
|
+
this.currentQuestion.closedReason = reason;
|
|
992
|
+
|
|
993
|
+
logWarn("[PendingQuestion]",
|
|
994
|
+
`Expired question ${this.currentQuestion.requestId}: ${reason}`);
|
|
995
|
+
|
|
996
|
+
if (this.jevDecisionCallback) {
|
|
997
|
+
this.jevDecisionCallback(this.currentQuestion, {
|
|
998
|
+
action: "stop",
|
|
999
|
+
reason,
|
|
1000
|
+
eligible: false,
|
|
1001
|
+
metadata: { outcome: "expired" },
|
|
631
1002
|
});
|
|
632
1003
|
}
|
|
633
1004
|
|
|
634
1005
|
return true;
|
|
635
1006
|
}
|
|
636
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* The state of the current question.
|
|
1010
|
+
*/
|
|
1011
|
+
getState(): QuestionState | undefined {
|
|
1012
|
+
return this.currentQuestion?.state;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
637
1015
|
/**
|
|
638
1016
|
* Apply an approval action from a channel to the current question.
|
|
639
1017
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/two-phase-review",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.450",
|
|
4
4
|
"description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|