pi-pier 0.2.6 → 0.3.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 +20 -7
- package/package.json +7 -2
- package/schemas/efficiency-config.schema.json +77 -0
- package/skills/pi-herdr-workflow/SKILL.md +13 -1
- package/src/ansi-text.ts +83 -0
- package/src/ask-multi.ts +260 -0
- package/src/ask-user.ts +458 -0
- package/src/bootstrap.ts +5 -2
- package/src/compact-coordinator.ts +454 -0
- package/src/compact-economics-core.ts +396 -0
- package/src/config-catalog-core.ts +452 -0
- package/src/config-command.ts +181 -0
- package/src/config-guide.ts +361 -0
- package/src/core/grid-shape.ts +4 -2
- package/src/core/heat-plan.ts +160 -0
- package/src/core/observation.ts +555 -0
- package/src/core/subagent.ts +187 -393
- package/src/core/terminal.ts +270 -47
- package/src/core/todo.ts +49 -14
- package/src/dashboard-command.ts +89 -0
- package/src/efficiency-config-core.ts +661 -0
- package/src/efficiency-config-io.ts +53 -0
- package/src/efficiency-store.ts +312 -0
- package/src/focus-poller.ts +244 -0
- package/src/gc-core.ts +85 -0
- package/src/git-adapter.ts +12 -3
- package/src/herdr-client.ts +292 -36
- package/src/index-locks.ts +27 -9
- package/src/index-master.ts +5 -0
- package/src/index-notices.ts +13 -2
- package/src/index-pipe.ts +5 -0
- package/src/index-worker.ts +5 -0
- package/src/index.ts +360 -60
- package/src/jev-client.ts +146 -0
- package/src/jev-core.ts +381 -0
- package/src/lock-core.ts +105 -16
- package/src/observation-core.ts +248 -0
- package/src/pier-options.ts +96 -0
- package/src/pipe-channel.ts +52 -7
- package/src/reducer-core.ts +358 -0
- package/src/reducer-invoker.ts +392 -0
- package/src/renderers.ts +198 -0
- package/src/role-loader.ts +2 -1
- package/src/role-manifest.ts +0 -4
- package/src/runtime-policy.ts +5 -8
- package/src/session-tail.ts +60 -5
- package/src/settle-wake-core.ts +3 -1
- package/src/slim-frame.ts +60 -3
- package/src/storage-layout.ts +23 -0
- package/src/subagent-core.ts +3 -1
- package/src/subagent-execute.ts +328 -0
- package/src/subagent-gc.ts +33 -13
- package/src/subagent-launch.ts +4 -0
- package/src/subagent-list-action.ts +69 -0
- package/src/subagent-output-action.ts +104 -0
- package/src/subagent-output-adapter.ts +27 -0
- package/src/subagent-output-core.ts +263 -0
- package/src/subagent-poll-loop.ts +59 -39
- package/src/subagent-poller.ts +46 -0
- package/src/subagent-ready.ts +89 -0
- package/src/subagent-registry.ts +70 -0
- package/src/subagent-resolution.ts +41 -0
- package/src/subagent-scope.ts +3 -1
- package/src/subagent-session-io.ts +115 -32
- package/src/subagent-spawn.ts +153 -8
- package/src/swallow.ts +75 -0
- package/src/terminal-core.ts +161 -10
- package/src/todo-core.ts +5 -4
- package/src/todo-reminder-core.ts +8 -1
- package/src/todos-service.ts +18 -6
- package/src/tool-error.ts +26 -0
- package/src/vocab.ts +7 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev decision layer — direct HTTP client (RFC docs/rfc-jev-integration.md §5).
|
|
3
|
+
*
|
|
4
|
+
* Why not the official SDK (@typesafe-ai/sdk v0.6.0): its retry machinery has
|
|
5
|
+
* no total-latency budget (DEFAULT_TIMEOUT_MS is per attempt, apiTimeoutError
|
|
6
|
+
* retries timeouts by default, Retry-After honored up to 60s — worst case
|
|
7
|
+
* >80s), which is incompatible with pier's fail-open budgets. The call surface
|
|
8
|
+
* here is one endpoint and three question types, and pi-pier ships as an npm
|
|
9
|
+
* package where a zero-dependency transport wins. Payload is pinned to the
|
|
10
|
+
* versioned model id; see the RFC for re-evaluation triggers.
|
|
11
|
+
*
|
|
12
|
+
* Every ask() resolves (never throws): errors, timeouts, 429s and low
|
|
13
|
+
* confidence at the call site all mean " unanswered" -> caller fails open.
|
|
14
|
+
*/
|
|
15
|
+
import { sha256Hex } from './observation-core.ts';
|
|
16
|
+
import { appendEfficiencyLog, efficiencyLogPath } from './efficiency-store.ts';
|
|
17
|
+
import { parseJevAnswers, type JevAnswer, type JevQuestion, type JevUsage } from './jev-core.ts';
|
|
18
|
+
import type { JevConfig } from './efficiency-config-core.ts';
|
|
19
|
+
|
|
20
|
+
export const JEV_DEFAULT_BASE_URL = 'https://api.typesafe.ai';
|
|
21
|
+
|
|
22
|
+
export interface JevAskMeta {
|
|
23
|
+
/** Stable question-set id for telemetry rows (e.g. 'epr-diagnostic-gate'). */
|
|
24
|
+
questionId: string;
|
|
25
|
+
/** Total-budget override in ms; defaults to the config timeoutMs. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
/** Site-specific static fields for telemetry; must never contain request bodies. */
|
|
29
|
+
extra?: Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Post-answer decision fields for telemetry (choices/scores/final verdicts —
|
|
32
|
+
* the data later threshold tuning needs; still never request bodies).
|
|
33
|
+
* Called exactly once per ask, success or failure.
|
|
34
|
+
*/
|
|
35
|
+
enrich?: (outcome: { ok: boolean; answers: Record<string, JevAnswer> | null }) => Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type JevAskResult =
|
|
39
|
+
| { readonly ok: true; readonly answers: Record<string, JevAnswer>; readonly model: string; readonly usage: JevUsage; readonly latencyMs: number }
|
|
40
|
+
| { readonly ok: false; readonly reason: string; readonly latencyMs: number };
|
|
41
|
+
|
|
42
|
+
export interface JevRuntimeDeps {
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
now?: () => number;
|
|
45
|
+
/** Session root for jev.jsonl; telemetry is skipped when absent. */
|
|
46
|
+
getSessionRoot?: () => string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface JevRuntime {
|
|
50
|
+
readonly available: boolean;
|
|
51
|
+
ask(request: { state: unknown; questions: Record<string, JevQuestion> }, meta: JevAskMeta): Promise<JevAskResult>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createJevRuntime(getConfig: () => JevConfig, deps: JevRuntimeDeps = {}): JevRuntime {
|
|
55
|
+
const doFetch = deps.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
56
|
+
const now = deps.now ?? Date.now;
|
|
57
|
+
|
|
58
|
+
async function logAsk(
|
|
59
|
+
config: JevConfig,
|
|
60
|
+
meta: JevAskMeta,
|
|
61
|
+
outcome: { ok: true; model: string; usage: JevUsage } | { ok: false; reason: string },
|
|
62
|
+
latencyMs: number,
|
|
63
|
+
state: unknown,
|
|
64
|
+
enriched: Record<string, unknown>,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (!config.logEnabled) return;
|
|
67
|
+
const sessionRoot = deps.getSessionRoot?.() ?? null;
|
|
68
|
+
if (!sessionRoot) return;
|
|
69
|
+
const serialized = JSON.stringify(state) ?? '';
|
|
70
|
+
const record: Record<string, unknown> = {
|
|
71
|
+
schema: 'pier-efficiency/1',
|
|
72
|
+
mechanism: 'jev',
|
|
73
|
+
ts: new Date(now()).toISOString(),
|
|
74
|
+
sessionId: meta.sessionId ?? 'unknown',
|
|
75
|
+
questionId: meta.questionId,
|
|
76
|
+
model: outcome.ok ? outcome.model : config.model,
|
|
77
|
+
latencyMs,
|
|
78
|
+
verdict: outcome.ok ? 'answered' : 'fallback',
|
|
79
|
+
fallback: !outcome.ok,
|
|
80
|
+
reason: outcome.ok ? null : outcome.reason,
|
|
81
|
+
stateBytes: Buffer.byteLength(serialized, 'utf8'),
|
|
82
|
+
stateHash: sha256Hex(serialized),
|
|
83
|
+
...(outcome.ok ? { usage: { input_tokens: outcome.usage.inputTokens, output_tokens: outcome.usage.outputTokens } } : {}),
|
|
84
|
+
...(meta.extra ?? {}),
|
|
85
|
+
...enriched,
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
await appendEfficiencyLog(efficiencyLogPath(sessionRoot, 'jev'), record);
|
|
89
|
+
} catch {
|
|
90
|
+
// Telemetry is best effort; never surface logging failures.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
// Getter: config reloads (session_start re-resolves from disk) propagate.
|
|
96
|
+
get available(): boolean {
|
|
97
|
+
const config = getConfig();
|
|
98
|
+
return config.enabled && Boolean(config.apiKey);
|
|
99
|
+
},
|
|
100
|
+
async ask(request, meta): Promise<JevAskResult> {
|
|
101
|
+
const startedAt = now();
|
|
102
|
+
const config = getConfig();
|
|
103
|
+
const fail = async (reason: string): Promise<JevAskResult> => {
|
|
104
|
+
const latencyMs = now() - startedAt;
|
|
105
|
+
const enriched = meta.enrich?.({ ok: false, answers: null }) ?? {};
|
|
106
|
+
await logAsk(config, meta, { ok: false, reason }, latencyMs, request.state, enriched);
|
|
107
|
+
return { ok: false, reason, latencyMs };
|
|
108
|
+
};
|
|
109
|
+
if (!config.enabled) return fail('disabled');
|
|
110
|
+
if (!config.apiKey) return fail('no-api-key');
|
|
111
|
+
|
|
112
|
+
const timeoutMs = meta.timeoutMs ?? config.timeoutMs;
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
115
|
+
try {
|
|
116
|
+
const base = (config.baseUrl ?? JEV_DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
117
|
+
const response = await doFetch(`${base}/v1/systemone`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({ model: config.model, state: request.state, questions: request.questions }),
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
// 429 (and everything else) fails open immediately: limits are
|
|
128
|
+
// dynamically adjusted upstream and every pier call is droppable.
|
|
129
|
+
return fail(response.status === 429 ? 'rate-limited' : `http-${response.status}`);
|
|
130
|
+
}
|
|
131
|
+
const raw: unknown = await response.json();
|
|
132
|
+
const parsed = parseJevAnswers(raw, Object.keys(request.questions));
|
|
133
|
+
if (!parsed.ok) return fail(parsed.reason);
|
|
134
|
+
const latencyMs = now() - startedAt;
|
|
135
|
+
const enriched = meta.enrich?.({ ok: true, answers: parsed.answers }) ?? {};
|
|
136
|
+
await logAsk(config, meta, { ok: true, model: parsed.model, usage: parsed.usage }, latencyMs, request.state, enriched);
|
|
137
|
+
return { ok: true, answers: parsed.answers, model: parsed.model, usage: parsed.usage, latencyMs };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const aborted = error instanceof Error && error.name === 'AbortError';
|
|
140
|
+
return fail(aborted ? 'timeout' : 'network-error');
|
|
141
|
+
} finally {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/jev-core.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev decision layer — pure core (RFC docs/rfc-jev-integration.md).
|
|
3
|
+
*
|
|
4
|
+
* Question/answer shapes mirror the closed set of POST /v1/systemone
|
|
5
|
+
* (https://docs.typesafe.ai/api): noul / choice / score. No I/O here; the HTTP
|
|
6
|
+
* client + jev.jsonl telemetry live in jev-client.ts.
|
|
7
|
+
*
|
|
8
|
+
* Design rules from the RFC:
|
|
9
|
+
* - arithmetic, counting and thresholds stay in code (jaggedness #2/#3);
|
|
10
|
+
* - state is minimal — only the fields each question needs (jaggedness #5);
|
|
11
|
+
* - instructions are English (models page: English is the primary training
|
|
12
|
+
* language; CJK is handled but weaker, so P0-2 runs a stricter gate);
|
|
13
|
+
* - any answer below its confidence gate counts as unanswered -> caller
|
|
14
|
+
* fails open to the existing heuristic.
|
|
15
|
+
*/
|
|
16
|
+
import { FAILURE_SIGNAL } from './reducer-core.ts';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Request/response shapes (closed set)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export type JevQuestion =
|
|
23
|
+
| { readonly type: 'noul'; readonly instructions: string; readonly criteria?: { readonly true?: string; readonly false?: string } }
|
|
24
|
+
| { readonly type: 'choice'; readonly instructions: string; readonly criteria: Record<string, string> }
|
|
25
|
+
| { readonly type: 'score'; readonly instructions: string; readonly criteria: readonly string[] };
|
|
26
|
+
|
|
27
|
+
export interface JevUsage {
|
|
28
|
+
readonly inputTokens: number;
|
|
29
|
+
readonly outputTokens: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type JevAnswer =
|
|
33
|
+
| { readonly type: 'noul'; readonly noul: number }
|
|
34
|
+
| { readonly type: 'choice'; readonly choice: string; readonly probabilities: Record<string, number>; readonly confidence: number }
|
|
35
|
+
| { readonly type: 'score'; readonly score: number; readonly probabilities: Record<string, number>; readonly confidence: number };
|
|
36
|
+
|
|
37
|
+
export interface JevRequest {
|
|
38
|
+
readonly state: unknown;
|
|
39
|
+
readonly questions: Record<string, JevQuestion>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type JevParsed =
|
|
43
|
+
| { readonly ok: true; readonly answers: Record<string, JevAnswer>; readonly model: string; readonly usage: JevUsage }
|
|
44
|
+
| { readonly ok: false; readonly reason: string };
|
|
45
|
+
|
|
46
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
47
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function finiteNumber(v: unknown): number | undefined {
|
|
51
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function unitNumber(v: unknown): number | undefined {
|
|
55
|
+
const n = finiteNumber(v);
|
|
56
|
+
return n !== undefined && n >= 0 && n <= 1 ? n : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseProbabilities(v: unknown): Record<string, number> | undefined {
|
|
60
|
+
if (!isPlainObject(v)) return undefined;
|
|
61
|
+
const out: Record<string, number> = {};
|
|
62
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
63
|
+
const n = finiteNumber(raw);
|
|
64
|
+
if (n === undefined) return undefined;
|
|
65
|
+
out[k] = n;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Strict validation of one response against the question ids we sent.
|
|
72
|
+
* Extra answer ids are ignored; every expected id must be present and
|
|
73
|
+
* well-formed or the whole call counts as unanswered (fail-open).
|
|
74
|
+
*/
|
|
75
|
+
export function parseJevAnswers(raw: unknown, expectedIds: readonly string[]): JevParsed {
|
|
76
|
+
if (!isPlainObject(raw)) return { ok: false, reason: 'response-not-object' };
|
|
77
|
+
const model = typeof raw.model === 'string' && raw.model.trim() ? raw.model : undefined;
|
|
78
|
+
if (model === undefined) return { ok: false, reason: 'missing-model' };
|
|
79
|
+
const rawUsage = isPlainObject(raw.usage) ? raw.usage : undefined;
|
|
80
|
+
const inputTokens = finiteNumber(rawUsage?.input_tokens);
|
|
81
|
+
const outputTokens = finiteNumber(rawUsage?.output_tokens);
|
|
82
|
+
if (inputTokens === undefined || inputTokens < 0 || outputTokens === undefined || outputTokens < 0) {
|
|
83
|
+
return { ok: false, reason: 'bad-usage' };
|
|
84
|
+
}
|
|
85
|
+
const rawAnswers = isPlainObject(raw.answers) ? raw.answers : undefined;
|
|
86
|
+
if (rawAnswers === undefined) return { ok: false, reason: 'missing-answers' };
|
|
87
|
+
|
|
88
|
+
const answers: Record<string, JevAnswer> = {};
|
|
89
|
+
for (const id of expectedIds) {
|
|
90
|
+
const a = isPlainObject(rawAnswers[id]) ? rawAnswers[id] : undefined;
|
|
91
|
+
if (a === undefined) return { ok: false, reason: `missing-answer:${id}` };
|
|
92
|
+
if (a.type === 'noul') {
|
|
93
|
+
const noul = unitNumber(a.noul);
|
|
94
|
+
if (noul === undefined) return { ok: false, reason: `bad-noul:${id}` };
|
|
95
|
+
answers[id] = { type: 'noul', noul };
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (a.type === 'choice') {
|
|
99
|
+
const choice = typeof a.choice === 'string' ? a.choice : undefined;
|
|
100
|
+
const probabilities = parseProbabilities(a.probabilities);
|
|
101
|
+
const confidence = unitNumber(a.confidence);
|
|
102
|
+
if (choice === undefined || probabilities === undefined || confidence === undefined) {
|
|
103
|
+
return { ok: false, reason: `bad-choice:${id}` };
|
|
104
|
+
}
|
|
105
|
+
answers[id] = { type: 'choice', choice, probabilities, confidence };
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (a.type === 'score') {
|
|
109
|
+
const score = finiteNumber(a.score);
|
|
110
|
+
const probabilities = parseProbabilities(a.probabilities);
|
|
111
|
+
const confidence = unitNumber(a.confidence);
|
|
112
|
+
if (score === undefined || score < 0 || probabilities === undefined || confidence === undefined) {
|
|
113
|
+
return { ok: false, reason: `bad-score:${id}` };
|
|
114
|
+
}
|
|
115
|
+
answers[id] = { type: 'score', score, probabilities, confidence };
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
return { ok: false, reason: `unknown-answer-type:${id}` };
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, answers, model, usage: { inputTokens, outputTokens } };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// P0-1: EPR diagnostic-command gate
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
export const DIAGNOSTIC_OUTPUT_MIN_NOUL = 0.6;
|
|
128
|
+
|
|
129
|
+
const DIAGNOSTIC_GATE_QUESTIONS: Record<string, JevQuestion> = {
|
|
130
|
+
cmd_kind: {
|
|
131
|
+
type: 'choice',
|
|
132
|
+
instructions: 'What kind of shell command is this?',
|
|
133
|
+
criteria: {
|
|
134
|
+
build_test_run: 'Runs a build, test, lint, or typecheck pipeline whose output is repetitive diagnostic log lines (pass/fail, errors, warnings)',
|
|
135
|
+
install_deps: 'Installs or updates dependencies (npm/pip/cargo fetch); output is progress bars and version lists',
|
|
136
|
+
server_watch: 'Starts a long-running server, watcher, or REPL that stays in the foreground',
|
|
137
|
+
source_inspection: 'Prints or searches source/config (cat, ls, grep, git log, diff)',
|
|
138
|
+
other: 'None of the above',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
diagnostic_output: {
|
|
142
|
+
type: 'noul',
|
|
143
|
+
instructions: 'Will this command produce repetitive test/build diagnostic log output (rather than conversational or interactive output)?',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function diagnosticGateRequest(command: string): JevRequest {
|
|
148
|
+
return { state: { command }, questions: DIAGNOSTIC_GATE_QUESTIONS };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface DiagnosticGateVerdict {
|
|
152
|
+
readonly hit: boolean;
|
|
153
|
+
readonly reason: string;
|
|
154
|
+
readonly confidence: number | null;
|
|
155
|
+
/** Selected cmd_kind option (telemetry); null when the answer was malformed. */
|
|
156
|
+
readonly choice: string | null;
|
|
157
|
+
/** diagnostic_output noul value (telemetry); null when the answer was malformed. */
|
|
158
|
+
readonly noul: number | null;
|
|
159
|
+
}
|
|
160
|
+
export function evaluateDiagnosticGate(
|
|
161
|
+
answers: Record<string, JevAnswer>,
|
|
162
|
+
minConfidence: number,
|
|
163
|
+
): DiagnosticGateVerdict {
|
|
164
|
+
const kind = answers.cmd_kind;
|
|
165
|
+
const output = answers.diagnostic_output;
|
|
166
|
+
if (kind === undefined || output === undefined || kind.type !== 'choice' || output.type !== 'noul') {
|
|
167
|
+
return { hit: false, reason: 'missing-answer', confidence: null, choice: null, noul: null };
|
|
168
|
+
}
|
|
169
|
+
if (kind.choice !== 'build_test_run') {
|
|
170
|
+
return { hit: false, reason: `not-build-test:${kind.choice}`, confidence: kind.confidence, choice: kind.choice, noul: output.noul };
|
|
171
|
+
}
|
|
172
|
+
if (kind.confidence < minConfidence) {
|
|
173
|
+
return { hit: false, reason: 'low-confidence', confidence: kind.confidence, choice: kind.choice, noul: output.noul };
|
|
174
|
+
}
|
|
175
|
+
if (output.noul < DIAGNOSTIC_OUTPUT_MIN_NOUL) {
|
|
176
|
+
return { hit: false, reason: 'non-diagnostic-output', confidence: kind.confidence, choice: kind.choice, noul: output.noul };
|
|
177
|
+
}
|
|
178
|
+
return { hit: true, reason: 'hit', confidence: kind.confidence, choice: kind.choice, noul: output.noul };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// P0-2: settlement-notice relevance ranking
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Stricter than the global minConfidence default: settlement text is mostly
|
|
187
|
+
* Chinese while English is jev's primary training language (models page), so
|
|
188
|
+
* the initial gate starts at 0.7 and calibrates from jev.jsonl.
|
|
189
|
+
*/
|
|
190
|
+
export const NOTICE_RANK_MIN_CONFIDENCE = 0.7;
|
|
191
|
+
export const RANK_SCORE_WEIGHT = 0.6;
|
|
192
|
+
|
|
193
|
+
const RANK_LEVELS = [
|
|
194
|
+
'Routine completion; the master agent can check it later',
|
|
195
|
+
'The master agent should be aware; it affects planning',
|
|
196
|
+
'Reports a failure, error, or blocked state needing immediate attention',
|
|
197
|
+
] as const;
|
|
198
|
+
|
|
199
|
+
export function noticeRankRequest(input: {
|
|
200
|
+
inProgressTodos: readonly string[];
|
|
201
|
+
notices: readonly string[];
|
|
202
|
+
}): JevRequest {
|
|
203
|
+
const questions: Record<string, JevQuestion> = {};
|
|
204
|
+
for (let i = 0; i < input.notices.length; i++) {
|
|
205
|
+
// Question keys are NOT sent to the model, so identical instructions would
|
|
206
|
+
// collapse every notice into the same question (observed live: 5x "1.98:0.97").
|
|
207
|
+
// Each instruction must name its settlement by state index (cookbook pattern).
|
|
208
|
+
questions[`notice_${i}_rank`] = {
|
|
209
|
+
type: 'score',
|
|
210
|
+
instructions: `How urgently does the master agent need to see settlements[${i}] right now, given in_progress_todos?`,
|
|
211
|
+
criteria: RANK_LEVELS,
|
|
212
|
+
};
|
|
213
|
+
questions[`notice_${i}_fail`] = {
|
|
214
|
+
type: 'noul',
|
|
215
|
+
instructions: `Does settlements[${i}] report a failure, error, or blocked state?`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
state: {
|
|
220
|
+
in_progress_todos: input.inProgressTodos,
|
|
221
|
+
settlements: input.notices,
|
|
222
|
+
},
|
|
223
|
+
questions,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Failure-pin threshold: a settlement whose fail-noul clears this is forced to
|
|
229
|
+
* the front regardless of its relevance score. Live calibration (2026-09-18):
|
|
230
|
+
* two runs over the same Chinese batch reordered a failure notice from #2 to
|
|
231
|
+
* #4 — beyond the 3-shown cap. Score noise on CJK must not be able to bury a
|
|
232
|
+
* failure; the invariant lives in code, not in the model.
|
|
233
|
+
*/
|
|
234
|
+
export const NOTICE_FAIL_PIN_THRESHOLD = 0.7;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Compose the display order: pinned failures first, then descending composed
|
|
238
|
+
* relevance (stable on ties -> arrival order). Per-item confidence gate: an
|
|
239
|
+
* item whose score answer is below the gate sinks to the routine bucket
|
|
240
|
+
* instead of voiding the batch (one unsure routine notice must not reorder
|
|
241
|
+
* everything back to arrival). All items gated -> null (nothing was answered).
|
|
242
|
+
* The fail pin itself ignores the score gate: noul answers carry no confidence
|
|
243
|
+
* field, and failure visibility is the safety property.
|
|
244
|
+
*/
|
|
245
|
+
export function composeNoticeRanking(
|
|
246
|
+
noticeCount: number,
|
|
247
|
+
answers: Record<string, JevAnswer>,
|
|
248
|
+
minConfidence: number,
|
|
249
|
+
): number[] | null {
|
|
250
|
+
const entries: Array<{ pin: number; value: number; index: number }> = [];
|
|
251
|
+
let answered = 0;
|
|
252
|
+
for (let i = 0; i < noticeCount; i++) {
|
|
253
|
+
const rank = answers[`notice_${i}_rank`];
|
|
254
|
+
const fail = answers[`notice_${i}_fail`];
|
|
255
|
+
if (rank === undefined || fail === undefined || rank.type !== 'score' || fail.type !== 'noul') {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const gated = rank.confidence < minConfidence;
|
|
259
|
+
if (!gated) answered++;
|
|
260
|
+
const normalizedScore = rank.score / Math.max(1, RANK_LEVELS.length - 1);
|
|
261
|
+
entries.push({
|
|
262
|
+
pin: fail.noul >= NOTICE_FAIL_PIN_THRESHOLD ? 1 : 0,
|
|
263
|
+
value: gated ? 0 : RANK_SCORE_WEIGHT * normalizedScore + (1 - RANK_SCORE_WEIGHT) * fail.noul,
|
|
264
|
+
index: i,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (answered === 0) return null;
|
|
268
|
+
entries.sort((a, b) => (b.pin - a.pin) || (b.value - a.value) || (a.index - b.index));
|
|
269
|
+
return entries.map((entry) => entry.index);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// P0-3: OBS excerpt window candidates (head/tail halves miss the middle)
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
export type ExcerptWindowId = 'first_signal' | 'densest';
|
|
277
|
+
|
|
278
|
+
export interface ExcerptWindow {
|
|
279
|
+
readonly id: ExcerptWindowId;
|
|
280
|
+
readonly label: string;
|
|
281
|
+
readonly text: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface LineWindow {
|
|
285
|
+
readonly text: string;
|
|
286
|
+
readonly bytes: number;
|
|
287
|
+
readonly signalCount: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildLineWindow(lines: readonly string[], start: number, budgetBytes: number, signalLines: ReadonlySet<number>): LineWindow | null {
|
|
291
|
+
const selected: string[] = [];
|
|
292
|
+
let bytes = 0;
|
|
293
|
+
let signalCount = 0;
|
|
294
|
+
for (let i = start; i < lines.length; i++) {
|
|
295
|
+
const line = lines[i]!;
|
|
296
|
+
const lineBytes = Buffer.byteLength(line, 'utf8') + 1; // +1 newline
|
|
297
|
+
if (bytes + lineBytes > budgetBytes) {
|
|
298
|
+
if (selected.length === 0) return null; // anchor line alone exceeds the budget
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
selected.push(line);
|
|
302
|
+
bytes += lineBytes;
|
|
303
|
+
if (signalLines.has(i)) signalCount++;
|
|
304
|
+
}
|
|
305
|
+
if (selected.length === 0) return null;
|
|
306
|
+
return { text: selected.join('\n'), bytes, signalCount };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Generate code-owned middle-window candidates for a packed observation:
|
|
311
|
+
* the region around the first failure-signal line, and the densest cluster
|
|
312
|
+
* of failure-signal lines within one half-budget. Deterministic, no model.
|
|
313
|
+
*/
|
|
314
|
+
export function buildExcerptWindows(text: string, halfBudgetBytes: number): ExcerptWindow[] {
|
|
315
|
+
if (halfBudgetBytes <= 0) return [];
|
|
316
|
+
const lines = text.split('\n');
|
|
317
|
+
const signalLines = new Set<number>();
|
|
318
|
+
for (let i = 0; i < lines.length; i++) {
|
|
319
|
+
if (FAILURE_SIGNAL.test(lines[i]!)) signalLines.add(i);
|
|
320
|
+
}
|
|
321
|
+
if (signalLines.size === 0) return [];
|
|
322
|
+
|
|
323
|
+
const out: ExcerptWindow[] = [];
|
|
324
|
+
const seen = new Set<string>();
|
|
325
|
+
|
|
326
|
+
const first = Math.min(...signalLines);
|
|
327
|
+
const firstWindow = buildLineWindow(lines, Math.max(0, first - 2), halfBudgetBytes, signalLines);
|
|
328
|
+
if (firstWindow !== null && !seen.has(firstWindow.text)) {
|
|
329
|
+
seen.add(firstWindow.text);
|
|
330
|
+
out.push({ id: 'first_signal', label: 'first failure-signal region', text: firstWindow.text });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Densest cluster: anchor at every signal line, extend greedily while in budget.
|
|
334
|
+
let best: LineWindow | null = null;
|
|
335
|
+
for (const anchor of signalLines) {
|
|
336
|
+
const window = buildLineWindow(lines, anchor, halfBudgetBytes, signalLines);
|
|
337
|
+
if (window === null) continue;
|
|
338
|
+
if (best === null || window.signalCount > best.signalCount) best = window;
|
|
339
|
+
}
|
|
340
|
+
if (best !== null && !seen.has(best.text)) {
|
|
341
|
+
seen.add(best.text);
|
|
342
|
+
out.push({ id: 'densest', label: 'densest failure-signal region', text: best.text });
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function excerptPickRequest(
|
|
348
|
+
middleWindows: readonly ExcerptWindow[],
|
|
349
|
+
headExcerpt: string,
|
|
350
|
+
tailExcerpt: string,
|
|
351
|
+
): JevRequest | null {
|
|
352
|
+
if (middleWindows.length === 0) return null;
|
|
353
|
+
const criteria: Record<string, string> = {
|
|
354
|
+
keep_head_tail: 'The existing head and tail excerpts are already the most informative; keep them',
|
|
355
|
+
};
|
|
356
|
+
const excerpts: Record<string, string> = { head_excerpt: headExcerpt, tail_excerpt: tailExcerpt };
|
|
357
|
+
for (const window of middleWindows) {
|
|
358
|
+
criteria[window.id] = `${window.label}: excerpt from the middle of the output`;
|
|
359
|
+
excerpts[window.id] = window.text;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
state: { excerpts },
|
|
363
|
+
questions: {
|
|
364
|
+
window: {
|
|
365
|
+
type: 'choice',
|
|
366
|
+
instructions: 'A large tool output was replaced by a placeholder keeping two short excerpts. Which excerpt set would most help a coding agent locate the problem without recalling the full text?',
|
|
367
|
+
criteria,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function evaluateExcerptPick(
|
|
374
|
+
answers: Record<string, JevAnswer>,
|
|
375
|
+
minConfidence: number,
|
|
376
|
+
): ExcerptWindowId | null {
|
|
377
|
+
const picked = answers.window;
|
|
378
|
+
if (picked === undefined || picked.type !== 'choice') return null;
|
|
379
|
+
if (picked.confidence < minConfidence) return null;
|
|
380
|
+
return picked.choice === 'first_signal' || picked.choice === 'densest' ? picked.choice : null;
|
|
381
|
+
}
|