specpi 0.30.0 → 0.30.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.
- package/CHANGELOG.md +14 -0
- package/NPM_RELEASE.md +2 -2
- package/README.md +4 -5
- package/SECURITY_MODEL.md +10 -6
- package/extensions/jev-advisor/broker.mjs +87 -8
- package/extensions/jev-advisor/client.mjs +33 -10
- package/extensions/jev-advisor/consent.mjs +19 -7
- package/extensions/jev-advisor/gate.mjs +20 -5
- package/extensions/jev-advisor/index.ts +307 -189
- package/extensions/jev-advisor/ledger.mjs +16 -2
- package/extensions/jev-advisor/questions/gap.mjs +49 -86
- package/extensions/jev-advisor/questions/retention.mjs +5 -8
- package/extensions/jev-advisor/questions/sources.mjs +27 -56
- package/extensions/jev-advisor/sanitize.mjs +0 -0
- package/extensions/tool-wishlist/core.mjs +86 -0
- package/extensions/tool-wishlist/index.ts +81 -8
- package/extensions/workflow-controls/index.ts +23 -0
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ import { consentPath, granted, revokeConsent } from "./consent.mjs";
|
|
|
7
7
|
import { createBroker } from "./broker.mjs";
|
|
8
8
|
import { ledgerPath, read as readLedger } from "./ledger.mjs";
|
|
9
9
|
import { usagePath } from "./usage.mjs";
|
|
10
|
+
import { compact } from "./sanitize.mjs";
|
|
10
11
|
import * as retention from "./questions/retention.mjs";
|
|
11
12
|
import * as gap from "./questions/gap.mjs";
|
|
12
13
|
import * as sources from "./questions/sources.mjs";
|
|
@@ -27,6 +28,48 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
27
28
|
const broker = createBroker({ loadSettings: () => settings });
|
|
28
29
|
const recent: { tool: string; outcome: string }[] = [];
|
|
29
30
|
let objective = "";
|
|
31
|
+
let currentRequest = "";
|
|
32
|
+
let taskIdentity = "";
|
|
33
|
+
let taskGeneration = 0;
|
|
34
|
+
let sessionActive = false;
|
|
35
|
+
|
|
36
|
+
const refreshObjective = async (ctx: ExtensionContext) => {
|
|
37
|
+
if (!sessionActive) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const generation = taskGeneration;
|
|
42
|
+
const replies: Promise<{ objective: string; digest: string } | undefined>[] = [];
|
|
43
|
+
let contract;
|
|
44
|
+
try {
|
|
45
|
+
if (settings.master) {
|
|
46
|
+
pi.events.emit("specpi:task-objective", {
|
|
47
|
+
ctx,
|
|
48
|
+
reply: (value: Promise<{ objective: string; digest: string } | undefined>) => replies.push(value),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
contract = replies.length === 1 ? await replies[0] : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
// A missing or unavailable workflow owner must not fail an otherwise valid tool.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (generation !== taskGeneration) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const next = compact(contract?.objective || currentRequest, 180);
|
|
62
|
+
const identity = contract?.digest ?? currentRequest;
|
|
63
|
+
if (objective !== next || taskIdentity !== identity) {
|
|
64
|
+
objective = next;
|
|
65
|
+
taskIdentity = identity;
|
|
66
|
+
taskGeneration += 1;
|
|
67
|
+
recent.length = 0;
|
|
68
|
+
resetTaskHistory();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
30
73
|
|
|
31
74
|
// System 5's local state. Every field here is something the session already knows; it exists so
|
|
32
75
|
// that "ask local state first" has something to ask. Local state cannot answer whether a session
|
|
@@ -57,7 +100,14 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
57
100
|
history.askedAtTurn = undefined;
|
|
58
101
|
};
|
|
59
102
|
|
|
60
|
-
const
|
|
103
|
+
const resetTaskHistory = () => {
|
|
104
|
+
const nudged = history.nudged;
|
|
105
|
+
resetHistory();
|
|
106
|
+
// Changing task context must not reset the existing once-per-session steering bound.
|
|
107
|
+
history.nudged = nudged;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const enabled = (system: string) => sessionActive && settings.master && settings.systems[system] === true;
|
|
61
111
|
|
|
62
112
|
// System 6: decide once, before the first provider request, whether this session will need a
|
|
63
113
|
// withdrawn tool group -- and offer it now rather than at turn 6.
|
|
@@ -80,12 +130,16 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
80
130
|
};
|
|
81
131
|
|
|
82
132
|
pi.on("session_start", () => {
|
|
133
|
+
sessionActive = true;
|
|
83
134
|
settings = loadSettings();
|
|
84
135
|
if (!settings.startup) {
|
|
85
136
|
settings = { ...settings, master: false };
|
|
86
137
|
}
|
|
87
138
|
|
|
88
139
|
broker.reset();
|
|
140
|
+
objective = "";
|
|
141
|
+
currentRequest = "";
|
|
142
|
+
taskGeneration += 1;
|
|
89
143
|
recent.length = 0;
|
|
90
144
|
resetHistory();
|
|
91
145
|
capabilityAsked = false;
|
|
@@ -96,7 +150,11 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
96
150
|
// finish, not reset: the counts are published once more as an ended session so anything
|
|
97
151
|
// reading them from outside -- SpecPi Chat's panel, most of all -- shows what the session
|
|
98
152
|
// actually spent rather than a zeroed live one.
|
|
153
|
+
sessionActive = false;
|
|
99
154
|
broker.finish();
|
|
155
|
+
objective = "";
|
|
156
|
+
currentRequest = "";
|
|
157
|
+
taskGeneration += 1;
|
|
100
158
|
recent.length = 0;
|
|
101
159
|
resetHistory();
|
|
102
160
|
});
|
|
@@ -106,14 +164,29 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
106
164
|
history.changedThisTurn = false;
|
|
107
165
|
});
|
|
108
166
|
|
|
109
|
-
// The
|
|
110
|
-
//
|
|
111
|
-
pi.on("before_agent_start", (event: any) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
167
|
+
// The workflow owner reads its active contract, not rendered Markdown. Without one, use only
|
|
168
|
+
// the bounded current request; never recover an objective by searching stored conversations.
|
|
169
|
+
pi.on("before_agent_start", async (event: any, ctx: ExtensionContext) => {
|
|
170
|
+
currentRequest = compact(event?.prompt ?? "", 180);
|
|
171
|
+
taskGeneration += 1;
|
|
172
|
+
pi.events.emit("specpi:task-changing");
|
|
173
|
+
await refreshObjective(ctx);
|
|
174
|
+
});
|
|
175
|
+
pi.on("input", async (event: any, ctx: ExtensionContext) => {
|
|
176
|
+
if (event.streamingBehavior === "steer" && event.source !== "extension") {
|
|
177
|
+
currentRequest = compact(event.text ?? "", 180);
|
|
178
|
+
taskGeneration += 1;
|
|
179
|
+
pi.events.emit("specpi:task-changing");
|
|
180
|
+
await refreshObjective(ctx);
|
|
115
181
|
}
|
|
116
182
|
});
|
|
183
|
+
pi.on("session_tree", () => {
|
|
184
|
+
objective = "";
|
|
185
|
+
currentRequest = "";
|
|
186
|
+
taskGeneration += 1;
|
|
187
|
+
recent.length = 0;
|
|
188
|
+
resetTaskHistory();
|
|
189
|
+
});
|
|
117
190
|
|
|
118
191
|
// Once per session, whatever the answer: asking later would be the mid-session flip the probe
|
|
119
192
|
// priced at three times the cost of doing it now.
|
|
@@ -166,8 +239,11 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
166
239
|
return;
|
|
167
240
|
}
|
|
168
241
|
|
|
169
|
-
const
|
|
242
|
+
const generation = taskGeneration;
|
|
243
|
+
await broker.request({
|
|
170
244
|
system: "capability",
|
|
245
|
+
isCurrent: () => generation === taskGeneration,
|
|
246
|
+
profile: "capability",
|
|
171
247
|
state: capabilities.buildInput({
|
|
172
248
|
prompt: event?.prompt,
|
|
173
249
|
reasons: local.reasons,
|
|
@@ -180,44 +256,51 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
180
256
|
decide: (answers: any) => {
|
|
181
257
|
const advice = capabilities.decide(answers, available);
|
|
182
258
|
|
|
183
|
-
return {
|
|
259
|
+
return { decision: advice };
|
|
184
260
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"info",
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
for (const id of advice.propose) {
|
|
201
|
-
const capability = table.findCapability(id);
|
|
202
|
-
if (!capability || capabilityDeclined.has(id)) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
261
|
+
apply: async (advice: any) => {
|
|
262
|
+
const effects: string[] = [];
|
|
263
|
+
if (advice.suggestDelegation) {
|
|
264
|
+
// A suggestion, never an activation: delegation binds a model and a host and has
|
|
265
|
+
// its own command, which is why the capability table deliberately omits it.
|
|
266
|
+
ctx.ui.notify(
|
|
267
|
+
"Jev: this looks like a question a delegated read-only session could answer over many files. Run /delegate on if you want it.",
|
|
268
|
+
"info",
|
|
269
|
+
);
|
|
270
|
+
effects.push("notification");
|
|
271
|
+
}
|
|
205
272
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
273
|
+
for (const id of advice.propose) {
|
|
274
|
+
const capability = table.findCapability(id);
|
|
275
|
+
if (!capability || capabilityDeclined.has(id)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const pending = table.missingTools(pi.getActiveTools(), capability);
|
|
280
|
+
// The same confirmation `request_capability` shows, pre-filled and moved to turn 0.
|
|
281
|
+
// Authority is unchanged: the human still decides, and declining is remembered so
|
|
282
|
+
// nothing asks twice in one session.
|
|
283
|
+
const accepted = await ctx.ui.confirm(
|
|
284
|
+
`Allow ${capability.label} for this session?`,
|
|
285
|
+
`Jev expects this request to ${capability.summary}, from the request itself rather than from anything it has done yet.\n\nThis offers ${pending.length} tool${pending.length === 1 ? "" : "s"} for the rest of this session and adds ${capability.schemaCost}. Accepting now is materially cheaper than accepting later: activating it mid-session also discards the cached prompt prefix, which measured about 20% of a mid-length attempt's cost. Withdraw it with ${capability.command} off.`,
|
|
286
|
+
);
|
|
287
|
+
effects.push("capability-proposal");
|
|
288
|
+
if (generation !== taskGeneration || !enabled("capability") || ctx.signal?.aborted) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!accepted) {
|
|
293
|
+
capabilityDeclined.add(id);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
syncActiveTools(pi, capability.tools, true);
|
|
298
|
+
effects.push("capability-activated");
|
|
299
|
+
}
|
|
218
300
|
|
|
219
|
-
|
|
220
|
-
|
|
301
|
+
return { applied: effects.length > 0, effects };
|
|
302
|
+
},
|
|
303
|
+
});
|
|
221
304
|
} catch {
|
|
222
305
|
// Nothing here may prevent a session from starting.
|
|
223
306
|
}
|
|
@@ -226,6 +309,11 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
226
309
|
// System 1: condense a spent tool result before it is appended. Doing this after the fact would
|
|
227
310
|
// rewrite a cached prefix; on arrival it never touches one.
|
|
228
311
|
pi.on("tool_result", async (event: any, ctx: ExtensionContext) => {
|
|
312
|
+
if (!(await refreshObjective(ctx))) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const generation = taskGeneration;
|
|
229
317
|
// Bookkeeping first, and unconditionally. Retention's own eligibility gate returns early on
|
|
230
318
|
// most results, and a history that only recorded the large read-only ones would be blind to
|
|
231
319
|
// exactly the short repeated failures system 5 exists to notice.
|
|
@@ -271,6 +359,8 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
271
359
|
// interested. System 7's own budget therefore only binds when retention is off or
|
|
272
360
|
// the result was too small for it.
|
|
273
361
|
system: wantRetention ? "retention" : "untrusted",
|
|
362
|
+
profile: wantRetention ? "retention" : "untrusted",
|
|
363
|
+
isCurrent: () => generation === taskGeneration,
|
|
274
364
|
state: retention.buildInput({ event, objective, recent }),
|
|
275
365
|
questions: {
|
|
276
366
|
...(wantRetention ? retention.questions() : {}),
|
|
@@ -283,17 +373,22 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
283
373
|
// because its length is the saving: computing it a second time to measure it would
|
|
284
374
|
// be the measurement inventing its own number.
|
|
285
375
|
decide: (answers: any) => {
|
|
286
|
-
|
|
287
|
-
? retention.decide(answers)
|
|
288
|
-
: { elide: false, reason: "retention-off" };
|
|
376
|
+
let verdict = wantRetention ? retention.decide(answers) : { elide: false, reason: "retention-off" };
|
|
289
377
|
const flagged = wantUntrusted && untrusted.decide(answers).banner;
|
|
290
378
|
// Order matters: shorten first, then mark. A banner belongs at the top of
|
|
291
379
|
// whatever the model is actually going to read.
|
|
292
380
|
const body = verdict.elide ? retention.digest(text, { tool: event.toolName, bytes }) : text;
|
|
293
|
-
|
|
381
|
+
let replacement = flagged ? untrusted.mark(body) : body;
|
|
382
|
+
if (verdict.elide && Buffer.byteLength(replacement, "utf8") >= bytes) {
|
|
383
|
+
verdict = { elide: false, reason: "no-byte-saving" };
|
|
384
|
+
replacement = flagged ? untrusted.mark(text) : text;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const marked = flagged && replacement !== body;
|
|
294
388
|
|
|
295
389
|
return {
|
|
296
|
-
applied: verdict.elide ||
|
|
390
|
+
applied: verdict.elide || marked,
|
|
391
|
+
effects: [...(verdict.elide ? ["elision"] : []), ...(marked ? ["warning"] : [])],
|
|
297
392
|
savedBytes: verdict.elide ? bytes - Buffer.byteLength(replacement, "utf8") : 0,
|
|
298
393
|
// retention.decide already names why it declined; carrying that into the
|
|
299
394
|
// ledger is what makes "asked and did nothing" diagnosable later.
|
|
@@ -334,6 +429,7 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
334
429
|
});
|
|
335
430
|
|
|
336
431
|
pi.on("tool_call", async (event: any, ctx: ExtensionContext) => {
|
|
432
|
+
const generation = taskGeneration;
|
|
337
433
|
history.signatures.push(progress.signature(event.toolName, event.input));
|
|
338
434
|
history.tools.push(event.toolName);
|
|
339
435
|
if (history.signatures.length > HISTORY_WINDOW) {
|
|
@@ -344,115 +440,132 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
344
440
|
history.tools.shift();
|
|
345
441
|
}
|
|
346
442
|
|
|
347
|
-
// System
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
(built.blockForSanitization ? 1 : 0) +
|
|
365
|
-
(built.canonicalKey && typeof event.input?.canonicalKey !== "string" ? 1 : 0) +
|
|
366
|
-
(built.suggestedFix && !event.input?.suggestedFix ? 1 : 0) +
|
|
367
|
-
(built.independentImpact ? 1 : 0);
|
|
368
|
-
|
|
369
|
-
return { applied: changes > 0, decision: built };
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
if (!result.ok) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const advice = result.decision;
|
|
377
|
-
if (advice.blockForSanitization) {
|
|
378
|
-
return {
|
|
379
|
-
block: true,
|
|
380
|
-
reason: "This report appears to contain a credential, an absolute path or other machine-specific detail. Rewrite it with the specifics removed and report it again.",
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (advice.canonicalKey && typeof event.input?.canonicalKey !== "string") {
|
|
385
|
-
event.input.canonicalKey = advice.canonicalKey;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (advice.suggestedFix && !event.input?.suggestedFix) {
|
|
389
|
-
event.input.suggestedFix = advice.suggestedFix;
|
|
390
|
-
}
|
|
443
|
+
// System 4: order the sources a delegation batch will snapshot. Ordering only — the same
|
|
444
|
+
// set is frozen either way, but a child pages through `list_sources` in this order.
|
|
445
|
+
if (
|
|
446
|
+
event.toolName === "delegate" &&
|
|
447
|
+
enabled("sources") &&
|
|
448
|
+
event.input?.operation === "run" &&
|
|
449
|
+
Array.isArray(event.input?.packet?.jobs)
|
|
450
|
+
) {
|
|
451
|
+
for (const job of event.input.packet.jobs) {
|
|
452
|
+
try {
|
|
453
|
+
if (
|
|
454
|
+
!Array.isArray(job.sources) ||
|
|
455
|
+
job.sources.length < 2 ||
|
|
456
|
+
!job.sources.every((item: unknown) => typeof item === "string")
|
|
457
|
+
) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
391
460
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
461
|
+
const original = [...job.sources];
|
|
462
|
+
const candidates = original.map((item: string) => ({ path: item }));
|
|
463
|
+
await broker.request({
|
|
464
|
+
system: "sources",
|
|
465
|
+
profile: "sources",
|
|
466
|
+
isCurrent: () => generation === taskGeneration && sessionActive,
|
|
467
|
+
state: sources.buildInput({ question: job.question, mode: job.mode, candidates }),
|
|
468
|
+
questions: sources.questions({ candidates }),
|
|
469
|
+
ctx,
|
|
470
|
+
root: ctx.cwd,
|
|
471
|
+
decide: (answers: any) => ({ decision: sources.decide(answers, candidates) }),
|
|
472
|
+
apply: (ranked: any, { recordEffect }: any) => {
|
|
473
|
+
const ordered = ranked.ordered.map((item: any) => item.path);
|
|
474
|
+
const moved = ordered.some((item: string, index: number) => item !== original[index]);
|
|
475
|
+
// No deduplication: even multiplicity and ungated positions are preserved.
|
|
476
|
+
job.sources = ordered;
|
|
477
|
+
const effects = moved ? ["sources-reordered"] : [];
|
|
478
|
+
if (moved) {
|
|
479
|
+
recordEffect("sources-reordered");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (ranked.notWorthDelegating && ctx.hasUI) {
|
|
483
|
+
ctx.ui.notify(
|
|
484
|
+
`Jev rates this ${job.mode} job a poor fit for delegation. Running anyway; ${ordered.length} sources remain selected.`,
|
|
485
|
+
"warning",
|
|
486
|
+
);
|
|
487
|
+
effects.push("warning");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { applied: effects.length > 0, effects };
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
} catch {
|
|
494
|
+
// One unavailable job must not prevent the next job or the delegation call.
|
|
396
495
|
}
|
|
397
|
-
} catch {
|
|
398
|
-
return;
|
|
399
496
|
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
400
499
|
|
|
500
|
+
// Wishlist owns collection and persistence. This handshake runs only after its local consent
|
|
501
|
+
// gate; advisory fields never enter model-authored tool arguments or its authority decisions.
|
|
502
|
+
pi.events.on("specpi:gap-triage", (request: any) => {
|
|
503
|
+
if (!enabled("gap")) {
|
|
401
504
|
return;
|
|
402
505
|
}
|
|
403
506
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
.map((item: string) => ({ path: item }));
|
|
411
|
-
if (candidates.length < 2) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
507
|
+
request.reply(
|
|
508
|
+
(async () => {
|
|
509
|
+
const existing = gap.shortlist(request.existing(), request.gap);
|
|
510
|
+
let stored: any;
|
|
511
|
+
let blocked = false;
|
|
512
|
+
let storageError: unknown;
|
|
415
513
|
const result = await broker.request({
|
|
416
|
-
system: "
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
514
|
+
system: "gap",
|
|
515
|
+
profile: "gap",
|
|
516
|
+
state: gap.buildInput({ gap: request.gap, existing }),
|
|
517
|
+
questions: gap.questions({ existing }),
|
|
518
|
+
ctx: request.ctx,
|
|
519
|
+
root: request.ctx.cwd,
|
|
520
|
+
signal: request.signal,
|
|
521
|
+
isCurrent: request.isCurrent,
|
|
522
|
+
decide: (answers: any) => ({ decision: gap.decide(answers, existing) }),
|
|
523
|
+
apply: async (advice: any, { isCurrent, recordEffect }: any) => {
|
|
524
|
+
if (advice.blockForSanitization) {
|
|
525
|
+
blocked = true;
|
|
526
|
+
|
|
527
|
+
return { applied: true, effects: ["report-blocked"] };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
stored = await request.record(advice, {
|
|
532
|
+
isCurrent,
|
|
533
|
+
onRecorded: ({ assessmentRecorded }: any) => {
|
|
534
|
+
if (assessmentRecorded) {
|
|
535
|
+
recordEffect("assessment-recorded");
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
storageError = error;
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
427
543
|
|
|
428
|
-
return {
|
|
544
|
+
return {
|
|
545
|
+
applied: stored.assessmentRecorded,
|
|
546
|
+
effects: stored.assessmentRecorded ? ["assessment-recorded"] : [],
|
|
547
|
+
};
|
|
429
548
|
},
|
|
430
549
|
});
|
|
431
|
-
if (
|
|
432
|
-
|
|
550
|
+
if (storageError) {
|
|
551
|
+
throw storageError;
|
|
433
552
|
}
|
|
434
553
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
event.input.sources = [...ordered, ...missing];
|
|
438
|
-
|
|
439
|
-
// Two answers the same batch already computed and nothing read. Output is free, so
|
|
440
|
-
// they were paid for whether or not anyone looked. A confident "this is not a
|
|
441
|
-
// self-contained evidence question" is worth surfacing before specpi-delegation
|
|
442
|
-
// freezes up to 200 files and 8 MiB for a child that then cannot answer it.
|
|
443
|
-
//
|
|
444
|
-
// Advisory only, and deliberately so: the batch still runs, the ceilings are
|
|
445
|
-
// unchanged, and with no UI this says nothing rather than blocking.
|
|
446
|
-
if (ranked.notWorthDelegating && ctx.hasUI) {
|
|
447
|
-
ctx.ui.notify(
|
|
448
|
-
`Jev rates this a poor fit for delegation${ranked.jobMode ? ` (it reads as ${ranked.jobMode} work)` : ""}. Running anyway; ${ordered.length + missing.length} sources will be frozen for the child.`,
|
|
449
|
-
"warning",
|
|
450
|
-
);
|
|
554
|
+
if (blocked) {
|
|
555
|
+
return { blocked: true };
|
|
451
556
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
557
|
+
|
|
558
|
+
if (stored) {
|
|
559
|
+
return { stored };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (["session-changed", "context-changed"].includes(result.reason)) {
|
|
563
|
+
return { cancelled: true };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return undefined;
|
|
567
|
+
})(),
|
|
568
|
+
);
|
|
456
569
|
});
|
|
457
570
|
|
|
458
571
|
// System 5: notice a session that has stopped making progress, while it can still be helped.
|
|
@@ -478,10 +591,13 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
478
591
|
// Recorded before the call rather than after, so a slow answer cannot let the next turn ask
|
|
479
592
|
// again while this one is still in flight.
|
|
480
593
|
history.askedAtTurn = history.turn;
|
|
594
|
+
const generation = taskGeneration;
|
|
481
595
|
void (async () => {
|
|
482
596
|
try {
|
|
483
|
-
|
|
597
|
+
await broker.request({
|
|
484
598
|
system: "progress",
|
|
599
|
+
profile: "progress",
|
|
600
|
+
isCurrent: () => generation === taskGeneration,
|
|
485
601
|
state: progress.buildInput({ history, objective, reasons: local.reasons }),
|
|
486
602
|
questions: progress.questions(),
|
|
487
603
|
ctx,
|
|
@@ -490,53 +606,54 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
490
606
|
const advice = progress.decide(answers);
|
|
491
607
|
|
|
492
608
|
return {
|
|
493
|
-
applied: Boolean(advice.nudge),
|
|
494
609
|
reason: advice.nudge ? advice.mode : advice.stuck ? "stuck-but-mode-ungated" : "not-stuck",
|
|
495
610
|
decision: advice,
|
|
496
611
|
};
|
|
497
612
|
},
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
613
|
+
apply: (advice: any) => {
|
|
614
|
+
const effects = [];
|
|
615
|
+
if (!advice.nudge || history.nudged) {
|
|
616
|
+
return { applied: false };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (ctx.hasUI) {
|
|
620
|
+
ctx.ui.notify(
|
|
621
|
+
advice.needsHuman
|
|
622
|
+
? `Jev progress check: this session looks blocked on something only you can answer. ${advice.nudge}`
|
|
623
|
+
: `Jev progress check: ${advice.nudge}`,
|
|
624
|
+
"warning",
|
|
625
|
+
);
|
|
626
|
+
effects.push("notification");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Queue one fixed instruction, never trigger an extra turn. Queueing is
|
|
630
|
+
// observable; eventual consumption by the model is not claimed here.
|
|
631
|
+
if (
|
|
632
|
+
settings.progressNudge === "message" &&
|
|
633
|
+
!advice.needsHuman &&
|
|
634
|
+
typeof pi.sendMessage === "function"
|
|
635
|
+
) {
|
|
636
|
+
pi.sendMessage(
|
|
637
|
+
{
|
|
638
|
+
customType: "specpi-jev-progress",
|
|
639
|
+
content: advice.nudge,
|
|
640
|
+
display: true,
|
|
641
|
+
details: { mode: advice.mode, reasons: local.reasons },
|
|
642
|
+
},
|
|
643
|
+
{ deliverAs: "steer" },
|
|
644
|
+
);
|
|
645
|
+
effects.push("steering-queued");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
history.nudged = effects.length > 0;
|
|
515
649
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
// Suppressed when the session is blocked on something only a person can answer:
|
|
524
|
-
// steering a model past a missing credential costs a turn to say nothing.
|
|
525
|
-
if (
|
|
526
|
-
settings.progressNudge === "message" &&
|
|
527
|
-
!result.decision.needsHuman &&
|
|
528
|
-
typeof pi.sendMessage === "function"
|
|
529
|
-
) {
|
|
530
|
-
pi.sendMessage(
|
|
531
|
-
{
|
|
532
|
-
customType: "specpi-jev-progress",
|
|
533
|
-
content: result.decision.nudge,
|
|
534
|
-
display: true,
|
|
535
|
-
details: { mode: result.decision.mode, reasons: local.reasons },
|
|
536
|
-
},
|
|
537
|
-
{ deliverAs: "steer" },
|
|
538
|
-
);
|
|
539
|
-
}
|
|
650
|
+
return {
|
|
651
|
+
applied: effects.length > 0,
|
|
652
|
+
effects,
|
|
653
|
+
reason: effects.length > 0 ? advice.mode : "no-delivery-channel",
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
});
|
|
540
657
|
} catch {
|
|
541
658
|
// The turn has already ended. An advisor must not be able to fail it retroactively.
|
|
542
659
|
}
|
|
@@ -696,9 +813,10 @@ export default function jevAdvisor(pi: ExtensionAPI) {
|
|
|
696
813
|
return;
|
|
697
814
|
}
|
|
698
815
|
|
|
699
|
-
const lines = entries.map(
|
|
700
|
-
|
|
701
|
-
`${entry.at} ${entry.system}
|
|
816
|
+
const lines = entries.map((entry: any) =>
|
|
817
|
+
entry.sent === false
|
|
818
|
+
? `${entry.at} ${entry.system} local abstention: ${entry.reason}`
|
|
819
|
+
: `${entry.at} ${entry.system} ${entry.stateBytes}B ${entry.ok ? `${entry.latencyMs}ms` : entry.reason} ${String(entry.payloadSha256 ?? "").slice(0, 12)} [${(entry.questionKeys ?? []).join(", ")}]`,
|
|
702
820
|
);
|
|
703
821
|
ctx.ui.notify(`${lines.join("\n")}\n\nLedger: ${ledgerPath()}`, "info");
|
|
704
822
|
|