@miller-tech/uap 1.87.0 → 1.89.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/dist/.tsbuildinfo +1 -1
- package/dist/cli/react.d.ts.map +1 -1
- package/dist/cli/react.js +10 -0
- package/dist/cli/react.js.map +1 -1
- package/dist/coordination/recipe-signal.d.ts +18 -0
- package/dist/coordination/recipe-signal.d.ts.map +1 -0
- package/dist/coordination/recipe-signal.js +85 -0
- package/dist/coordination/recipe-signal.js.map +1 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/confidence_escalation.py +116 -9
- package/tools/agents/tests/test_confidence_escalation.py +101 -3
package/dist/cli/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/cli/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/cli/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/F;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,MAAM,CAO3E;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AASD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF"}
|
package/dist/cli/react.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from '../coordination/reactor.js';
|
|
2
|
+
import { maybeWriteRecipeSignal } from '../coordination/recipe-signal.js';
|
|
2
3
|
/**
|
|
3
4
|
* Pure core of `uap react`: parse a JSON ReactorContext payload, resolve the
|
|
4
5
|
* dynamic UAP capabilities for the event, and return the JSON ReactorResult.
|
|
@@ -59,5 +60,14 @@ export async function reactCommand(options = {}) {
|
|
|
59
60
|
if (options.maxInjectChars)
|
|
60
61
|
opts.maxInjectChars = Number(options.maxInjectChars);
|
|
61
62
|
process.stdout.write(runReact(payload, opts) + '\n');
|
|
63
|
+
// Cross-process: stamp the recipe signal for this prompt so the serving-layer
|
|
64
|
+
// proxy can route on the reactor's actual capability/complexity. Best-effort.
|
|
65
|
+
try {
|
|
66
|
+
const promptText = JSON.parse(payload).promptText ?? '';
|
|
67
|
+
maybeWriteRecipeSignal(promptText);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* fail open */
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
//# sourceMappingURL=react.js.map
|
package/dist/cli/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/cli/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA4C,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/cli/react.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type CapabilityRouter } from './capability-router.js';
|
|
2
|
+
export interface RecipeSignal {
|
|
3
|
+
ts: number;
|
|
4
|
+
promptHash: string;
|
|
5
|
+
complexity: string;
|
|
6
|
+
capabilities: string[];
|
|
7
|
+
shape: string;
|
|
8
|
+
recipe: string;
|
|
9
|
+
confidence: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function normalizePrompt(text: string): string;
|
|
12
|
+
export declare function promptHash(text: string): string;
|
|
13
|
+
export declare function computeRecipeSignal(promptText: string, router?: CapabilityRouter): RecipeSignal;
|
|
14
|
+
export declare function signalDir(dir?: string): string;
|
|
15
|
+
export declare function writeRecipeSignal(sig: RecipeSignal, dir?: string): void;
|
|
16
|
+
/** Best-effort: compute + write the signal for a prompt. Never throws. */
|
|
17
|
+
export declare function maybeWriteRecipeSignal(promptText: string, dir?: string): void;
|
|
18
|
+
//# sourceMappingURL=recipe-signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe-signal.d.ts","sourceRoot":"","sources":["../../src/coordination/recipe-signal.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAQpF,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,gBAAwC,GAC/C,YAAY,CAgCd;AAED,wBAAgB,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAUvE;AAED,0EAA0E;AAC1E,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAO7E"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe signal (cross-process): the reactor writes per-prompt routing signals
|
|
3
|
+
* that the serving-layer proxy consumes for recipe selection. Keyed by a hash
|
|
4
|
+
* of the normalized prompt (identical algorithm to the Python proxy), plus a
|
|
5
|
+
* rolling latest.json for the single-session common case. Fails open.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { measureQueryComplexity } from '../utils/query-complexity.js';
|
|
12
|
+
import { getCapabilityRouter } from './capability-router.js';
|
|
13
|
+
const CODE_CAPS = new Set([
|
|
14
|
+
'typescript', 'javascript', 'python', 'rust', 'go', 'cpp', 'java',
|
|
15
|
+
'frontend', 'backend', 'cli', 'api-design',
|
|
16
|
+
]);
|
|
17
|
+
const REASONING_CAPS = new Set(['architecture', 'product', 'test-strategy', 'compliance']);
|
|
18
|
+
export function normalizePrompt(text) {
|
|
19
|
+
return (text || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
20
|
+
}
|
|
21
|
+
export function promptHash(text) {
|
|
22
|
+
return createHash('sha1').update(normalizePrompt(text)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
export function computeRecipeSignal(promptText, router = getCapabilityRouter()) {
|
|
25
|
+
const complexity = measureQueryComplexity(promptText); // simple | moderate | complex
|
|
26
|
+
let capabilities = [];
|
|
27
|
+
let confidence = 0;
|
|
28
|
+
try {
|
|
29
|
+
const r = router.routeTask({
|
|
30
|
+
id: 'react-signal', title: promptText, type: 'task', status: 'open',
|
|
31
|
+
priority: 2, labels: [], createdAt: '1970-01-01T00:00:00.000Z',
|
|
32
|
+
updatedAt: '1970-01-01T00:00:00.000Z',
|
|
33
|
+
});
|
|
34
|
+
capabilities = r.matchedCapabilities ?? [];
|
|
35
|
+
confidence = r.confidence ?? 0;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* fall back to text-only shape */
|
|
39
|
+
}
|
|
40
|
+
const shape = capabilities.some((c) => REASONING_CAPS.has(c))
|
|
41
|
+
? 'reasoning'
|
|
42
|
+
: capabilities.some((c) => CODE_CAPS.has(c))
|
|
43
|
+
? 'code'
|
|
44
|
+
: /\b(prove|why|explain|derive|which of|true or false)\b/i.test(promptText)
|
|
45
|
+
? 'reasoning'
|
|
46
|
+
: 'general';
|
|
47
|
+
const recipe = complexity === 'complex' || shape === 'reasoning' ? 'fusion' : 'confidence';
|
|
48
|
+
return {
|
|
49
|
+
ts: Date.now() / 1000,
|
|
50
|
+
promptHash: promptHash(promptText),
|
|
51
|
+
complexity,
|
|
52
|
+
capabilities,
|
|
53
|
+
shape,
|
|
54
|
+
recipe,
|
|
55
|
+
confidence,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function signalDir(dir) {
|
|
59
|
+
return dir || process.env.UAP_RECIPE_SIGNAL_DIR || join(homedir(), '.cache', 'uap', 'recipe-signals');
|
|
60
|
+
}
|
|
61
|
+
export function writeRecipeSignal(sig, dir) {
|
|
62
|
+
try {
|
|
63
|
+
const d = signalDir(dir);
|
|
64
|
+
if (!existsSync(d))
|
|
65
|
+
mkdirSync(d, { recursive: true });
|
|
66
|
+
const data = JSON.stringify(sig);
|
|
67
|
+
writeFileSync(join(d, `${sig.promptHash}.json`), data);
|
|
68
|
+
writeFileSync(join(d, 'latest.json'), data);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* fail open — the proxy falls back to its own signal extraction */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Best-effort: compute + write the signal for a prompt. Never throws. */
|
|
75
|
+
export function maybeWriteRecipeSignal(promptText, dir) {
|
|
76
|
+
if (!promptText || !promptText.trim())
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
writeRecipeSignal(computeRecipeSignal(promptText), dir);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* fail open */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=recipe-signal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe-signal.js","sourceRoot":"","sources":["../../src/coordination/recipe-signal.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAyB,MAAM,wBAAwB,CAAC;AAEpF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM;IACjE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY;CAC3C,CAAC,CAAC;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC;AAY3F,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,UAAkB,EAClB,SAA2B,mBAAmB,EAAE;IAEhD,MAAM,UAAU,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC,CAAC,8BAA8B;IACrF,IAAI,YAAY,GAAa,EAAE,CAAC;IAChC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC;YACzB,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;YACnE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,0BAA0B;YAC9D,SAAS,EAAE,0BAA0B;SAC7B,CAAC,CAAC;QACZ,YAAY,GAAI,CAAwC,CAAC,mBAAmB,IAAI,EAAE,CAAC;QACnF,UAAU,GAAI,CAA6B,CAAC,UAAU,IAAI,CAAC,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;IACD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3D,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1C,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,wDAAwD,CAAC,IAAI,CAAC,UAAU,CAAC;gBACzE,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,SAAS,CAAC;IAClB,MAAM,MAAM,GAAG,UAAU,KAAK,SAAS,IAAI,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC;IAC3F,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI;QACrB,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC;QAClC,UAAU;QACV,YAAY;QACZ,KAAK;QACL,MAAM;QACN,UAAU;KACX,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAY;IACpC,OAAO,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAiB,EAAE,GAAY;IAC/D,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,SAAS,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,UAAU,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;QACvD,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;IACrE,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,sBAAsB,CAAC,UAAkB,EAAE,GAAY;IACrE,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE;QAAE,OAAO;IAC9C,IAAI,CAAC;QACH,iBAAiB,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -27,8 +27,11 @@ Env:
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
29
|
import asyncio
|
|
30
|
+
import hashlib
|
|
31
|
+
import json
|
|
30
32
|
import os
|
|
31
33
|
import re
|
|
34
|
+
import time
|
|
32
35
|
from dataclasses import dataclass
|
|
33
36
|
|
|
34
37
|
|
|
@@ -39,6 +42,7 @@ class Settings:
|
|
|
39
42
|
signal: str
|
|
40
43
|
threshold: float
|
|
41
44
|
fusion_n: int
|
|
45
|
+
remom_quorum: int
|
|
42
46
|
auto_fusion_chars: int
|
|
43
47
|
model: str
|
|
44
48
|
endpoint: str
|
|
@@ -56,7 +60,7 @@ class Settings:
|
|
|
56
60
|
return default
|
|
57
61
|
|
|
58
62
|
recipe = os.environ.get("PROXY_RECIPE", "auto").lower()
|
|
59
|
-
if recipe not in {"auto", "single", "confidence", "fusion"}:
|
|
63
|
+
if recipe not in {"auto", "single", "confidence", "fusion", "ratings", "remom", "workflow"}:
|
|
60
64
|
recipe = "auto"
|
|
61
65
|
signal = os.environ.get("PROXY_CONFIDENCE_SIGNAL", "heuristic").lower()
|
|
62
66
|
if signal not in {"heuristic", "selfverify"}:
|
|
@@ -67,6 +71,7 @@ class Settings:
|
|
|
67
71
|
signal=signal,
|
|
68
72
|
threshold=num("PROXY_CONFIDENCE_THRESHOLD", 0.5, float),
|
|
69
73
|
fusion_n=max(2, min(6, num("PROXY_FUSION_N", 3, int))),
|
|
74
|
+
remom_quorum=max(1, min(6, num("PROXY_REMOM_QUORUM", 2, int))),
|
|
70
75
|
auto_fusion_chars=num("PROXY_AUTO_FUSION_CHARS", 600, int),
|
|
71
76
|
model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
|
|
72
77
|
endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
|
|
@@ -175,20 +180,66 @@ def task_signals(anthropic_body: dict) -> dict:
|
|
|
175
180
|
return {"complexity": query_complexity(text), "shape": task_shape(text)}
|
|
176
181
|
|
|
177
182
|
|
|
183
|
+
# ---- cross-process: consume the harness reactor's actual routeResult ----
|
|
184
|
+
# The reactor (TS, in the harness) writes a per-prompt signal keyed by a hash of
|
|
185
|
+
# the normalized prompt, plus a rolling latest.json. The proxy reads it here so
|
|
186
|
+
# recipe selection uses the reactor's real capability/complexity routing rather
|
|
187
|
+
# than re-deriving it. Falls back to the proxy's own signal extraction on miss.
|
|
188
|
+
def _default_signal_dir() -> str:
|
|
189
|
+
return os.environ.get("UAP_RECIPE_SIGNAL_DIR") or os.path.join(
|
|
190
|
+
os.path.expanduser("~"), ".cache", "uap", "recipe-signals"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def normalize_prompt(text: str) -> str:
|
|
195
|
+
return " ".join((text or "").strip().lower().split())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def prompt_hash(text: str) -> str:
|
|
199
|
+
return hashlib.sha1(normalize_prompt(text).encode("utf-8")).hexdigest()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_reactor_signal(prompt_text, signal_dir=None, ttl=180.0, now=None):
|
|
203
|
+
"""Return the reactor's signal dict for this prompt, or None. Exact
|
|
204
|
+
prompt-hash match first; then a recent latest.json fallback (single-session
|
|
205
|
+
common case). Fresh within ttl seconds. Never raises."""
|
|
206
|
+
d = signal_dir or _default_signal_dir()
|
|
207
|
+
now = time.time() if now is None else now
|
|
208
|
+
for path in (os.path.join(d, prompt_hash(prompt_text) + ".json"), os.path.join(d, "latest.json")):
|
|
209
|
+
try:
|
|
210
|
+
if os.path.exists(path):
|
|
211
|
+
with open(path) as f:
|
|
212
|
+
sig = json.load(f)
|
|
213
|
+
if now - float(sig.get("ts", 0)) <= ttl:
|
|
214
|
+
return sig
|
|
215
|
+
except Exception:
|
|
216
|
+
continue
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
178
220
|
def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) -> str:
|
|
179
221
|
if not settings.enabled or has_tools:
|
|
180
222
|
return "single"
|
|
181
223
|
if settings.recipe != "auto":
|
|
182
224
|
return settings.recipe
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
text = latest_user_text(anthropic_body)
|
|
226
|
+
# Prefer the harness reactor's ACTUAL routeResult signal when fresh; else the
|
|
227
|
+
# proxy's own signal extraction (faithful port of query-complexity).
|
|
228
|
+
rsig = load_reactor_signal(text)
|
|
229
|
+
if rsig is not None:
|
|
230
|
+
rec = rsig.get("recipe")
|
|
231
|
+
if rec in {"single", "confidence", "fusion"}:
|
|
232
|
+
if rec == "fusion" and not settings.backend_configured():
|
|
233
|
+
return "confidence"
|
|
234
|
+
return rec
|
|
235
|
+
complexity = rsig.get("complexity") or query_complexity(text)
|
|
236
|
+
shape = rsig.get("shape") or task_shape(text)
|
|
237
|
+
else:
|
|
238
|
+
complexity = query_complexity(text)
|
|
239
|
+
shape = task_shape(text)
|
|
240
|
+
if settings.backend_configured() and (complexity == "complex" or shape == "reasoning"):
|
|
189
241
|
return "fusion"
|
|
190
|
-
|
|
191
|
-
if settings.backend_configured() and len(latest_user_text(anthropic_body)) >= settings.auto_fusion_chars:
|
|
242
|
+
if settings.backend_configured() and len(text) >= settings.auto_fusion_chars:
|
|
192
243
|
return "fusion"
|
|
193
244
|
return "confidence"
|
|
194
245
|
|
|
@@ -272,6 +323,26 @@ def parse_judge_index(text: str, n: int):
|
|
|
272
323
|
return i if 0 <= i < n else None
|
|
273
324
|
|
|
274
325
|
|
|
326
|
+
def build_synthesis_payload(anthropic_body: dict, candidate_texts: list[str], settings: Settings) -> dict:
|
|
327
|
+
q = latest_user_text(anthropic_body)
|
|
328
|
+
listing = "\n\n".join(f"[{i}]\n{t}" for i, t in enumerate(candidate_texts))
|
|
329
|
+
prompt = (
|
|
330
|
+
"Synthesize a SINGLE best answer to the REQUEST by merging the correct, "
|
|
331
|
+
"complementary parts of the candidate answers below. Resolve disagreements "
|
|
332
|
+
"and keep the required output format. Reply with ONLY the final answer."
|
|
333
|
+
"\n\nREQUEST:\n" + q + "\n\nCANDIDATES:\n" + listing
|
|
334
|
+
)
|
|
335
|
+
return {"model": settings.model, "max_tokens": anthropic_body.get("max_tokens", 4096),
|
|
336
|
+
"messages": [{"role": "user", "content": prompt}]}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
async def _fanout_candidates(primary_resp, openai_body, settings, call_primary):
|
|
340
|
+
"""[primary] + up to fusion_n-1 temperature-varied breadth candidates."""
|
|
341
|
+
variants = build_fusion_variants(openai_body or {}, settings.fusion_n)
|
|
342
|
+
results = await asyncio.gather(*[call_primary(v) for v in variants], return_exceptions=True)
|
|
343
|
+
return [primary_resp] + [r for r in results if isinstance(r, dict)]
|
|
344
|
+
|
|
345
|
+
|
|
275
346
|
def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
|
|
276
347
|
"""Back-compat (heuristic confidence path)."""
|
|
277
348
|
if not settings.enabled or not settings.backend_configured() or has_tools:
|
|
@@ -321,6 +392,42 @@ async def apply_recipe(primary_resp, anthropic_body, openai_body, settings, has_
|
|
|
321
392
|
if idx is not None:
|
|
322
393
|
return candidates[idx]
|
|
323
394
|
return primary_resp # fallback to best valid evidence
|
|
395
|
+
|
|
396
|
+
if recipe == "ratings":
|
|
397
|
+
# Bounded ensemble: rate each candidate independently, pick the best.
|
|
398
|
+
candidates = await _fanout_candidates(primary_resp, openai_body, settings, call_primary)
|
|
399
|
+
if len(candidates) <= 1 or not settings.backend_configured() or call_judge is None:
|
|
400
|
+
return primary_resp
|
|
401
|
+
rs = await asyncio.gather(
|
|
402
|
+
*[call_judge(build_verify_payload(anthropic_body, extract_text(c), settings))
|
|
403
|
+
for c in candidates],
|
|
404
|
+
return_exceptions=True,
|
|
405
|
+
)
|
|
406
|
+
scored = []
|
|
407
|
+
for c, jr2 in zip(candidates, rs):
|
|
408
|
+
sc = parse_verify_score(extract_text(jr2)) if isinstance(jr2, dict) else None
|
|
409
|
+
scored.append((sc if sc is not None else text_confidence(extract_text(c)), c))
|
|
410
|
+
return max(scored, key=lambda x: x[0])[1]
|
|
411
|
+
|
|
412
|
+
if recipe == "remom":
|
|
413
|
+
# Breadth -> quorum -> synthesis into the output contract; fall back
|
|
414
|
+
# to the best valid evidence if synthesis fails (no API error).
|
|
415
|
+
candidates = await _fanout_candidates(primary_resp, openai_body, settings, call_primary)
|
|
416
|
+
valid = [c for c in candidates if extract_text(c).strip()]
|
|
417
|
+
if len(valid) >= settings.remom_quorum and settings.backend_configured() and call_judge is not None:
|
|
418
|
+
synth = await call_judge(
|
|
419
|
+
build_synthesis_payload(anthropic_body, [extract_text(c) for c in valid], settings)
|
|
420
|
+
)
|
|
421
|
+
if isinstance(synth, dict) and extract_text(synth).strip():
|
|
422
|
+
return synth
|
|
423
|
+
return max(valid, key=lambda c: len(extract_text(c))) if valid else primary_resp
|
|
424
|
+
|
|
425
|
+
if recipe == "workflow":
|
|
426
|
+
# Workflows (planner/patcher/verifier under a contract) are the
|
|
427
|
+
# deliver convergence loop's job — it owns the real execution +
|
|
428
|
+
# acceptance gates and repo state a stateless serving turn lacks.
|
|
429
|
+
# Pass through; the harness routes workflow tasks through uap deliver.
|
|
430
|
+
return primary_resp
|
|
324
431
|
except Exception:
|
|
325
432
|
return primary_resp
|
|
326
433
|
return primary_resp
|
|
@@ -9,14 +9,16 @@ mod_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts"
|
|
|
9
9
|
spec = importlib.util.spec_from_file_location("confidence_escalation", mod_path)
|
|
10
10
|
ce = importlib.util.module_from_spec(spec)
|
|
11
11
|
sys.modules["confidence_escalation"] = ce
|
|
12
|
+
import tempfile, os as _os2
|
|
13
|
+
_os2.environ["UAP_RECIPE_SIGNAL_DIR"] = tempfile.mkdtemp(prefix="uap-sig-")
|
|
12
14
|
spec.loader.exec_module(ce)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def S(enabled=True, recipe="auto", signal="heuristic", threshold=0.5, fusion_n=3,
|
|
16
|
-
auto_chars=600, model="opus", endpoint="http://x/", key="k"):
|
|
18
|
+
auto_chars=600, model="opus", endpoint="http://x/", key="k", remom_quorum=2):
|
|
17
19
|
return ce.Settings(enabled=enabled, recipe=recipe, signal=signal, threshold=threshold,
|
|
18
|
-
fusion_n=fusion_n,
|
|
19
|
-
endpoint=endpoint, api_key=key)
|
|
20
|
+
fusion_n=fusion_n, remom_quorum=remom_quorum, auto_fusion_chars=auto_chars,
|
|
21
|
+
model=model, endpoint=endpoint, api_key=key)
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def resp(text):
|
|
@@ -133,5 +135,101 @@ class ApplyFusionTest(unittest.TestCase): # #3
|
|
|
133
135
|
self.assertEqual(ce.extract_text(out), "c0")
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
|
|
139
|
+
class CrossProcessSignalTest(unittest.TestCase): # reactor -> proxy
|
|
140
|
+
def setUp(self):
|
|
141
|
+
import tempfile
|
|
142
|
+
self.d = tempfile.mkdtemp(prefix="uap-sig-test-")
|
|
143
|
+
|
|
144
|
+
def _write(self, name, sig):
|
|
145
|
+
import json, os
|
|
146
|
+
with open(os.path.join(self.d, name), "w") as f:
|
|
147
|
+
json.dump(sig, f)
|
|
148
|
+
|
|
149
|
+
def test_prompt_hash_match_consumed(self):
|
|
150
|
+
text = "build the thing"
|
|
151
|
+
h = ce.prompt_hash(text)
|
|
152
|
+
self._write(h + ".json", {"ts": __import__("time").time(), "recipe": "fusion"})
|
|
153
|
+
sig = ce.load_reactor_signal(text, signal_dir=self.d)
|
|
154
|
+
self.assertEqual(sig["recipe"], "fusion")
|
|
155
|
+
|
|
156
|
+
def test_stale_signal_ignored(self):
|
|
157
|
+
text = "old prompt"
|
|
158
|
+
self._write(ce.prompt_hash(text) + ".json", {"ts": 1, "recipe": "fusion"})
|
|
159
|
+
self.assertIsNone(ce.load_reactor_signal(text, signal_dir=self.d, ttl=180.0))
|
|
160
|
+
|
|
161
|
+
def test_latest_fallback(self):
|
|
162
|
+
self._write("latest.json", {"ts": __import__("time").time(), "recipe": "fusion"})
|
|
163
|
+
sig = ce.load_reactor_signal("anything not hashed", signal_dir=self.d)
|
|
164
|
+
self.assertEqual(sig["recipe"], "fusion")
|
|
165
|
+
|
|
166
|
+
def test_select_recipe_uses_reactor_recommendation(self):
|
|
167
|
+
text = "add a button" # self-extract would say confidence (simple)
|
|
168
|
+
self._write(ce.prompt_hash(text) + ".json",
|
|
169
|
+
{"ts": __import__("time").time(), "recipe": "fusion"})
|
|
170
|
+
import os
|
|
171
|
+
prev = os.environ.get("UAP_RECIPE_SIGNAL_DIR")
|
|
172
|
+
os.environ["UAP_RECIPE_SIGNAL_DIR"] = self.d
|
|
173
|
+
try:
|
|
174
|
+
r = ce.select_recipe(body(text), S(recipe="auto"), False)
|
|
175
|
+
finally:
|
|
176
|
+
if prev is None:
|
|
177
|
+
os.environ.pop("UAP_RECIPE_SIGNAL_DIR", None)
|
|
178
|
+
else:
|
|
179
|
+
os.environ["UAP_RECIPE_SIGNAL_DIR"] = prev
|
|
180
|
+
self.assertEqual(r, "fusion") # reactor signal overrides the simple self-classification
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ApplyRatingsTest(unittest.TestCase): # Ratings
|
|
185
|
+
def test_picks_highest_rated_candidate(self):
|
|
186
|
+
async def primary(v): return resp("cand-1")
|
|
187
|
+
scores = iter(["3", "9"]) # primary rated 3, fanout rated 9
|
|
188
|
+
async def judge(p): return resp(next(scores))
|
|
189
|
+
out = run(ce.apply_recipe(resp("cand-0"), body(), {"model": "q", "messages": []},
|
|
190
|
+
S(recipe="ratings", fusion_n=2), False, primary, judge))
|
|
191
|
+
self.assertEqual(ce.extract_text(out), "cand-1")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ApplyReMoMTest(unittest.TestCase): # ReMoM
|
|
195
|
+
def test_synthesizes_when_quorum_met(self):
|
|
196
|
+
async def primary(v): return resp("evidence B")
|
|
197
|
+
async def judge(p): return resp("MERGED ANSWER")
|
|
198
|
+
out = run(ce.apply_recipe(resp("evidence A"), body(), {"model": "q", "messages": []},
|
|
199
|
+
S(recipe="remom", fusion_n=2), False, primary, judge))
|
|
200
|
+
self.assertEqual(ce.extract_text(out), "MERGED ANSWER")
|
|
201
|
+
|
|
202
|
+
def test_falls_back_to_best_evidence_when_synthesis_fails(self):
|
|
203
|
+
async def primary(v): return resp("a longer, more complete candidate answer")
|
|
204
|
+
async def judge(p): return None # synthesis failed
|
|
205
|
+
out = run(ce.apply_recipe(resp("short"), body(), {"model": "q", "messages": []},
|
|
206
|
+
S(recipe="remom", fusion_n=2), False, primary, judge))
|
|
207
|
+
self.assertEqual(ce.extract_text(out), "a longer, more complete candidate answer")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class WorkflowTest(unittest.TestCase):
|
|
211
|
+
def test_workflow_passes_through(self): # Workflows = deliver's job
|
|
212
|
+
async def primary(v): return resp("x")
|
|
213
|
+
async def judge(p): return resp("y")
|
|
214
|
+
out = run(ce.apply_recipe(resp("PRIMARY"), body(), {}, S(recipe="workflow"), False, primary, judge))
|
|
215
|
+
self.assertEqual(ce.extract_text(out), "PRIMARY")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SettingsExtTest(unittest.TestCase):
|
|
219
|
+
def test_remom_quorum_default(self):
|
|
220
|
+
import os, tempfile
|
|
221
|
+
for k in ("PROXY_REMOM_QUORUM",): os.environ.pop(k, None)
|
|
222
|
+
self.assertEqual(ce.Settings.from_env().remom_quorum, 2)
|
|
223
|
+
|
|
224
|
+
def test_new_recipes_valid(self):
|
|
225
|
+
import os
|
|
226
|
+
for r in ("ratings", "remom", "workflow"):
|
|
227
|
+
os.environ["PROXY_RECIPE"] = r
|
|
228
|
+
try:
|
|
229
|
+
self.assertEqual(ce.Settings.from_env().recipe, r)
|
|
230
|
+
finally:
|
|
231
|
+
os.environ.pop("PROXY_RECIPE", None)
|
|
232
|
+
|
|
233
|
+
|
|
136
234
|
if __name__ == "__main__":
|
|
137
235
|
unittest.main()
|