myshell-tools 2.4.0 → 2.6.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/CHANGELOG.md +15 -0
- package/README.md +26 -10
- package/dist/cli.js +33 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/cost.js +4 -1
- package/dist/commands/cost.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.d.ts +41 -2
- package/dist/commands/login.js +116 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/core/assess.js +2 -62
- package/dist/core/assess.js.map +1 -1
- package/dist/core/budget.d.ts +26 -0
- package/dist/core/budget.js +37 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/history.d.ts +35 -0
- package/dist/core/history.js +116 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/json-envelope.d.ts +49 -0
- package/dist/core/json-envelope.js +117 -0
- package/dist/core/json-envelope.js.map +1 -0
- package/dist/core/orchestrate.js +107 -8
- package/dist/core/orchestrate.js.map +1 -1
- package/dist/core/policy.js +17 -9
- package/dist/core/policy.js.map +1 -1
- package/dist/core/prompt.d.ts +9 -4
- package/dist/core/prompt.js +14 -5
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/review.js +2 -49
- package/dist/core/review.js.map +1 -1
- package/dist/core/route.d.ts +13 -5
- package/dist/core/route.js +20 -6
- package/dist/core/route.js.map +1 -1
- package/dist/core/types.d.ts +37 -0
- package/dist/infra/pricing.d.ts +17 -4
- package/dist/infra/pricing.js +73 -3
- package/dist/infra/pricing.js.map +1 -1
- package/dist/interface/menu.d.ts +12 -0
- package/dist/interface/menu.js +106 -22
- package/dist/interface/menu.js.map +1 -1
- package/dist/providers/detect.d.ts +17 -5
- package/dist/providers/detect.js +56 -4
- package/dist/providers/detect.js.map +1 -1
- package/dist/providers/install.js +1 -0
- package/dist/providers/install.js.map +1 -1
- package/dist/providers/opencode-parse.d.ts +49 -0
- package/dist/providers/opencode-parse.js +181 -0
- package/dist/providers/opencode-parse.js.map +1 -0
- package/dist/providers/opencode.d.ts +43 -0
- package/dist/providers/opencode.js +121 -0
- package/dist/providers/opencode.js.map +1 -0
- package/dist/providers/port.d.ts +1 -1
- package/dist/providers/registry.d.ts +2 -2
- package/dist/providers/registry.js +6 -2
- package/dist/providers/registry.js.map +1 -1
- package/package.json +2 -2
package/dist/core/assess.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assess.js","sourceRoot":"","sources":["../../src/core/assess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"assess.js","sourceRoot":"","sources":["../../src/core/assess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;GAEG;AACH,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,SAAS,UAAU,CAAC,CAAU;IAC5B,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IACrC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,MAAc;IACnC,MAAM,WAAW,GAAe;QAC9B,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,wBAAwB;QAChC,WAAW,EAAE,KAAK;KACnB,CAAC;IAEF,kCAAkC;IAClC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,QAA4B,CAAC;IACjC,IAAI,CAAC;QACH,QAAQ,GAAG,qBAAqB,CAAC,MAAM,EAAE,YAAY,CAAuB,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,+CAA+C;IAC/C,IAAI,OAAO,QAAQ,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9E,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,MAAM,GACV,OAAO,QAAQ,CAAC,MAAM,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QACtE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE;QACxB,CAAC,CAAC,0BAA0B,CAAC;IAEjC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/budget.ts — pure per-task cost budget helpers.
|
|
3
|
+
*
|
|
4
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
5
|
+
* - No imports of fs / path / child_process
|
|
6
|
+
* - No console.* calls
|
|
7
|
+
* - No Date.now() / Math.random() / new Date()
|
|
8
|
+
* - No process.exit()
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when `spentUsd` has reached or exceeded the budget cap, so
|
|
12
|
+
* that orchestrate() must stop spending.
|
|
13
|
+
*
|
|
14
|
+
* Returns `false` (no cap) when:
|
|
15
|
+
* - `maxCostUsd` is `null` or `undefined`
|
|
16
|
+
* - `maxCostUsd` is ≤ 0 (non-positive cap is treated as "uncapped")
|
|
17
|
+
*/
|
|
18
|
+
export declare function budgetExceeded(spentUsd: number, maxCostUsd: number | null | undefined): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returns how many USD remain in the budget, or `null` when uncapped.
|
|
21
|
+
*
|
|
22
|
+
* The returned value may be zero or negative if the cap has already been
|
|
23
|
+
* reached; callers should use {@link budgetExceeded} to make gate decisions
|
|
24
|
+
* rather than checking the sign here.
|
|
25
|
+
*/
|
|
26
|
+
export declare function remainingBudget(spentUsd: number, maxCostUsd: number | null | undefined): number | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/budget.ts — pure per-task cost budget helpers.
|
|
3
|
+
*
|
|
4
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
5
|
+
* - No imports of fs / path / child_process
|
|
6
|
+
* - No console.* calls
|
|
7
|
+
* - No Date.now() / Math.random() / new Date()
|
|
8
|
+
* - No process.exit()
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when `spentUsd` has reached or exceeded the budget cap, so
|
|
12
|
+
* that orchestrate() must stop spending.
|
|
13
|
+
*
|
|
14
|
+
* Returns `false` (no cap) when:
|
|
15
|
+
* - `maxCostUsd` is `null` or `undefined`
|
|
16
|
+
* - `maxCostUsd` is ≤ 0 (non-positive cap is treated as "uncapped")
|
|
17
|
+
*/
|
|
18
|
+
export function budgetExceeded(spentUsd, maxCostUsd) {
|
|
19
|
+
if (maxCostUsd === null || maxCostUsd === undefined || maxCostUsd <= 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return spentUsd >= maxCostUsd;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns how many USD remain in the budget, or `null` when uncapped.
|
|
26
|
+
*
|
|
27
|
+
* The returned value may be zero or negative if the cap has already been
|
|
28
|
+
* reached; callers should use {@link budgetExceeded} to make gate decisions
|
|
29
|
+
* rather than checking the sign here.
|
|
30
|
+
*/
|
|
31
|
+
export function remainingBudget(spentUsd, maxCostUsd) {
|
|
32
|
+
if (maxCostUsd === null || maxCostUsd === undefined || maxCostUsd <= 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return maxCostUsd - spentUsd;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/core/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAgB,EAChB,UAAqC;IAErC,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,IAAI,UAAU,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,UAAqC;IAErC,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,UAAU,GAAG,QAAQ,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/history.ts — compact prior conversation history for context injection.
|
|
3
|
+
*
|
|
4
|
+
* Produces a bounded, human-readable summary of prior SessionEntry turns to be
|
|
5
|
+
* injected into the next provider prompt, giving stateless one-shot providers
|
|
6
|
+
* (claude -p / codex exec) awareness of earlier conversation context.
|
|
7
|
+
*
|
|
8
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
9
|
+
* - No imports of fs / path / child_process
|
|
10
|
+
* - No console.* calls
|
|
11
|
+
* - No Date.now() / Math.random() / new Date()
|
|
12
|
+
* - No process.exit()
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionEntry } from './types.js';
|
|
15
|
+
export interface CompactHistoryOptions {
|
|
16
|
+
/** Maximum total characters in the returned string. Default: 6000. */
|
|
17
|
+
readonly maxChars?: number;
|
|
18
|
+
/** Maximum number of conversation turns to include. Default: 12. */
|
|
19
|
+
readonly maxTurns?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compact prior conversation history into a bounded string for context injection.
|
|
23
|
+
*
|
|
24
|
+
* - Takes the MOST RECENT up-to-`maxTurns` entries (preserves chronological order).
|
|
25
|
+
* - Strips confidence-envelope JSON from assistant turns before including them.
|
|
26
|
+
* - Enforces `maxChars` by dropping the OLDEST included turns first until under budget.
|
|
27
|
+
* - If a single turn's content alone exceeds `maxChars`, truncates it with a marker.
|
|
28
|
+
* - Returns '' for empty / undefined input.
|
|
29
|
+
*
|
|
30
|
+
* Pure: no I/O, no Date, no Math.random. Never throws.
|
|
31
|
+
*
|
|
32
|
+
* @param entries - The prior conversation entries (oldest first).
|
|
33
|
+
* @param opts - Optional bounds overrides.
|
|
34
|
+
*/
|
|
35
|
+
export declare function compactHistory(entries: readonly SessionEntry[], opts?: CompactHistoryOptions): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/history.ts — compact prior conversation history for context injection.
|
|
3
|
+
*
|
|
4
|
+
* Produces a bounded, human-readable summary of prior SessionEntry turns to be
|
|
5
|
+
* injected into the next provider prompt, giving stateless one-shot providers
|
|
6
|
+
* (claude -p / codex exec) awareness of earlier conversation context.
|
|
7
|
+
*
|
|
8
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
9
|
+
* - No imports of fs / path / child_process
|
|
10
|
+
* - No console.* calls
|
|
11
|
+
* - No Date.now() / Math.random() / new Date()
|
|
12
|
+
* - No process.exit()
|
|
13
|
+
*/
|
|
14
|
+
import { lastJsonObjectBoundsWithKey } from './json-envelope.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const DEFAULT_MAX_CHARS = 6000;
|
|
19
|
+
const DEFAULT_MAX_TURNS = 12;
|
|
20
|
+
const TRUNCATION_MARKER = ' …[truncated]';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Internal helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Strip any trailing confidence-envelope JSON block from an assistant's content.
|
|
26
|
+
*
|
|
27
|
+
* The envelope is a trailing `{ ... }` object that contains `"confidence"`.
|
|
28
|
+
* Uses {@link lastJsonObjectBoundsWithKey} to locate the last such block, then
|
|
29
|
+
* removes it (and surrounding whitespace) from the content.
|
|
30
|
+
*
|
|
31
|
+
* Never throws — returns the original content on any parse failure.
|
|
32
|
+
*/
|
|
33
|
+
function stripEnvelope(content) {
|
|
34
|
+
try {
|
|
35
|
+
const match = lastJsonObjectBoundsWithKey(content, 'confidence');
|
|
36
|
+
if (match === null) {
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
// Remove the envelope and any leading whitespace/newline before it
|
|
40
|
+
const before = content.slice(0, match.start).replace(/\s+$/, '');
|
|
41
|
+
const after = content.slice(match.end).replace(/^\s+/, '');
|
|
42
|
+
return after.length > 0 ? `${before}\n${after}` : before;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Map a SessionEntry role to the display label used in the history block.
|
|
50
|
+
*/
|
|
51
|
+
function roleLabel(role) {
|
|
52
|
+
if (role === 'user')
|
|
53
|
+
return 'User';
|
|
54
|
+
if (role === 'assistant')
|
|
55
|
+
return 'Assistant';
|
|
56
|
+
return 'System';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compact prior conversation history into a bounded string for context injection.
|
|
60
|
+
*
|
|
61
|
+
* - Takes the MOST RECENT up-to-`maxTurns` entries (preserves chronological order).
|
|
62
|
+
* - Strips confidence-envelope JSON from assistant turns before including them.
|
|
63
|
+
* - Enforces `maxChars` by dropping the OLDEST included turns first until under budget.
|
|
64
|
+
* - If a single turn's content alone exceeds `maxChars`, truncates it with a marker.
|
|
65
|
+
* - Returns '' for empty / undefined input.
|
|
66
|
+
*
|
|
67
|
+
* Pure: no I/O, no Date, no Math.random. Never throws.
|
|
68
|
+
*
|
|
69
|
+
* @param entries - The prior conversation entries (oldest first).
|
|
70
|
+
* @param opts - Optional bounds overrides.
|
|
71
|
+
*/
|
|
72
|
+
export function compactHistory(entries, opts) {
|
|
73
|
+
try {
|
|
74
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
const maxChars = opts?.maxChars ?? DEFAULT_MAX_CHARS;
|
|
78
|
+
const maxTurns = opts?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
79
|
+
// Take the most recent up-to-maxTurns entries, then keep chronological order
|
|
80
|
+
const window = entries.slice(-maxTurns);
|
|
81
|
+
// Format each entry into a display line
|
|
82
|
+
const formatted = window.map((entry) => {
|
|
83
|
+
const label = roleLabel(entry.role);
|
|
84
|
+
const rawContent = entry.role === 'assistant' ? stripEnvelope(entry.content) : entry.content;
|
|
85
|
+
const content = rawContent.trim();
|
|
86
|
+
return `${label}: ${content}`;
|
|
87
|
+
});
|
|
88
|
+
// Enforce maxChars by dropping oldest turns first
|
|
89
|
+
// Each formatted turn is separated by '\n\n'
|
|
90
|
+
let kept = formatted.slice(); // copy so we can mutate
|
|
91
|
+
while (kept.length > 0) {
|
|
92
|
+
const joined = kept.join('\n\n');
|
|
93
|
+
if (joined.length <= maxChars) {
|
|
94
|
+
return joined;
|
|
95
|
+
}
|
|
96
|
+
// Drop the oldest turn
|
|
97
|
+
kept = kept.slice(1);
|
|
98
|
+
}
|
|
99
|
+
// If we get here, even a single turn is too long — truncate it
|
|
100
|
+
if (formatted.length > 0) {
|
|
101
|
+
const last = formatted[formatted.length - 1];
|
|
102
|
+
if (last !== undefined && last.length > maxChars) {
|
|
103
|
+
const truncated = last.slice(0, maxChars - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
104
|
+
return truncated;
|
|
105
|
+
}
|
|
106
|
+
if (last !== undefined) {
|
|
107
|
+
return last;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/core/history.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/json-envelope.ts — shared brace-depth JSON-envelope scanner.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable helpers that scan text for the LAST balanced `{...}` JSON
|
|
5
|
+
* object containing a given key. Used by assess.ts (confidence envelope),
|
|
6
|
+
* review.ts (verdict envelope), and history.ts (envelope stripping) to avoid
|
|
7
|
+
* duplicated scanning logic.
|
|
8
|
+
*
|
|
9
|
+
* Honesty Contract: these functions NEVER throw on any input. On parse failure
|
|
10
|
+
* or absent key they return null. They never fabricate data.
|
|
11
|
+
*
|
|
12
|
+
* Pure module: no I/O, no time, no randomness.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Scan `text` and return the LAST balanced `{...}` block that:
|
|
16
|
+
* 1. Parses as valid JSON,
|
|
17
|
+
* 2. Is a plain object (not null, not an array), and
|
|
18
|
+
* 3. Contains the given `key` as a direct property.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when no matching block is found. Never throws.
|
|
21
|
+
*
|
|
22
|
+
* Scanning semantics:
|
|
23
|
+
* - All `{` positions are tried left-to-right.
|
|
24
|
+
* - For each `{`, the matching `}` is located by tracking brace depth.
|
|
25
|
+
* - All candidates that parse and contain `key` are collected; the LAST one
|
|
26
|
+
* wins. This handles duplicate/regenerated envelopes in model output.
|
|
27
|
+
*
|
|
28
|
+
* @param text - The text to scan (any string).
|
|
29
|
+
* @param key - The property key that must be present in the JSON object.
|
|
30
|
+
*/
|
|
31
|
+
export declare function lastJsonObjectWithKey(text: string, key: string): Record<string, unknown> | null;
|
|
32
|
+
/**
|
|
33
|
+
* Like {@link lastJsonObjectWithKey}, but also returns the character offsets of
|
|
34
|
+
* the matched block within `text` so callers can excise it.
|
|
35
|
+
*
|
|
36
|
+
* Returns `null` when no matching block is found. Never throws.
|
|
37
|
+
*
|
|
38
|
+
* The returned `start` and `end` follow the same convention as
|
|
39
|
+
* `String.prototype.slice`: `text.slice(start, end)` reproduces the matched
|
|
40
|
+
* `{...}` block exactly.
|
|
41
|
+
*
|
|
42
|
+
* @param text - The text to scan (any string).
|
|
43
|
+
* @param key - The property key that must be present in the JSON object.
|
|
44
|
+
*/
|
|
45
|
+
export declare function lastJsonObjectBoundsWithKey(text: string, key: string): {
|
|
46
|
+
readonly start: number;
|
|
47
|
+
readonly end: number;
|
|
48
|
+
readonly value: Record<string, unknown>;
|
|
49
|
+
} | null;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/json-envelope.ts — shared brace-depth JSON-envelope scanner.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable helpers that scan text for the LAST balanced `{...}` JSON
|
|
5
|
+
* object containing a given key. Used by assess.ts (confidence envelope),
|
|
6
|
+
* review.ts (verdict envelope), and history.ts (envelope stripping) to avoid
|
|
7
|
+
* duplicated scanning logic.
|
|
8
|
+
*
|
|
9
|
+
* Honesty Contract: these functions NEVER throw on any input. On parse failure
|
|
10
|
+
* or absent key they return null. They never fabricate data.
|
|
11
|
+
*
|
|
12
|
+
* Pure module: no I/O, no time, no randomness.
|
|
13
|
+
*/
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Core scanning loop (private)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Walk `text` left-to-right, collecting every balanced `{...}` block that
|
|
19
|
+
* parses as a plain JSON object and contains `key`. Returns the last match
|
|
20
|
+
* (or null if none found). Never throws.
|
|
21
|
+
*/
|
|
22
|
+
function scanLast(text, key) {
|
|
23
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
let last = null;
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
const start = text.indexOf('{', i);
|
|
29
|
+
if (start === -1)
|
|
30
|
+
break;
|
|
31
|
+
// Walk forward tracking brace depth to find the matching '}'
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let j = start;
|
|
34
|
+
let foundClose = false;
|
|
35
|
+
while (j < text.length) {
|
|
36
|
+
if (text[j] === '{') {
|
|
37
|
+
depth++;
|
|
38
|
+
}
|
|
39
|
+
else if (text[j] === '}') {
|
|
40
|
+
depth--;
|
|
41
|
+
if (depth === 0) {
|
|
42
|
+
foundClose = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
j++;
|
|
47
|
+
}
|
|
48
|
+
if (foundClose) {
|
|
49
|
+
const candidate = text.slice(start, j + 1);
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(candidate);
|
|
52
|
+
if (parsed !== null &&
|
|
53
|
+
typeof parsed === 'object' &&
|
|
54
|
+
!Array.isArray(parsed) &&
|
|
55
|
+
key in parsed) {
|
|
56
|
+
last = { start, end: j + 1, value: parsed };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Not valid JSON — skip
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
i = start + 1;
|
|
64
|
+
}
|
|
65
|
+
return last;
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Public API
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* Scan `text` and return the LAST balanced `{...}` block that:
|
|
72
|
+
* 1. Parses as valid JSON,
|
|
73
|
+
* 2. Is a plain object (not null, not an array), and
|
|
74
|
+
* 3. Contains the given `key` as a direct property.
|
|
75
|
+
*
|
|
76
|
+
* Returns `null` when no matching block is found. Never throws.
|
|
77
|
+
*
|
|
78
|
+
* Scanning semantics:
|
|
79
|
+
* - All `{` positions are tried left-to-right.
|
|
80
|
+
* - For each `{`, the matching `}` is located by tracking brace depth.
|
|
81
|
+
* - All candidates that parse and contain `key` are collected; the LAST one
|
|
82
|
+
* wins. This handles duplicate/regenerated envelopes in model output.
|
|
83
|
+
*
|
|
84
|
+
* @param text - The text to scan (any string).
|
|
85
|
+
* @param key - The property key that must be present in the JSON object.
|
|
86
|
+
*/
|
|
87
|
+
export function lastJsonObjectWithKey(text, key) {
|
|
88
|
+
try {
|
|
89
|
+
const match = scanLast(text, key);
|
|
90
|
+
return match !== null ? match.value : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Like {@link lastJsonObjectWithKey}, but also returns the character offsets of
|
|
98
|
+
* the matched block within `text` so callers can excise it.
|
|
99
|
+
*
|
|
100
|
+
* Returns `null` when no matching block is found. Never throws.
|
|
101
|
+
*
|
|
102
|
+
* The returned `start` and `end` follow the same convention as
|
|
103
|
+
* `String.prototype.slice`: `text.slice(start, end)` reproduces the matched
|
|
104
|
+
* `{...}` block exactly.
|
|
105
|
+
*
|
|
106
|
+
* @param text - The text to scan (any string).
|
|
107
|
+
* @param key - The property key that must be present in the JSON object.
|
|
108
|
+
*/
|
|
109
|
+
export function lastJsonObjectBoundsWithKey(text, key) {
|
|
110
|
+
try {
|
|
111
|
+
return scanLast(text, key);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=json-envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-envelope.js","sourceRoot":"","sources":["../../src/core/json-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,8EAA8E;AAC9E,+BAA+B;AAC/B,8EAA8E;AAE9E;;;;GAIG;AACH,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE/D,IAAI,IAAI,GAAqB,IAAI,CAAC;IAElC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,MAAM;QAExB,6DAA6D;QAC7D,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,GAAG,KAAK,CAAC;QACd,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACpB,KAAK,EAAE,CAAC;YACV,CAAC;iBAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,KAAK,EAAE,CAAC;gBACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;oBAChB,UAAU,GAAG,IAAI,CAAC;oBAClB,MAAM;gBACR,CAAC;YACH,CAAC;YACD,CAAC,EAAE,CAAC;QACN,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3C,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC9C,IACE,MAAM,KAAK,IAAI;oBACf,OAAO,MAAM,KAAK,QAAQ;oBAC1B,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;oBACtB,GAAG,IAAK,MAAiB,EACzB,CAAC;oBACD,IAAI,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,MAAiC,EAAE,CAAC;gBACzE,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;QAED,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAY,EACZ,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAClC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,2BAA2B,CACzC,IAAY,EACZ,GAAW;IAEX,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/dist/core/orchestrate.js
CHANGED
|
@@ -30,13 +30,32 @@ import { classify } from './classify.js';
|
|
|
30
30
|
import { route } from './route.js';
|
|
31
31
|
import { buildPrompt } from './prompt.js';
|
|
32
32
|
import { assess } from './assess.js';
|
|
33
|
+
import { compactHistory } from './history.js';
|
|
33
34
|
import { getModelPricing, calculateCost } from '../infra/pricing.js';
|
|
34
35
|
import { nextTierUp, pickReviewer } from './escalate.js';
|
|
35
36
|
import { buildReviewPrompt, parseReviewVerdict } from './review.js';
|
|
37
|
+
import { budgetExceeded } from './budget.js';
|
|
36
38
|
// ---------------------------------------------------------------------------
|
|
37
|
-
// Pure helper: should this
|
|
39
|
+
// Pure helper: should this output be cross-vendor reviewed?
|
|
38
40
|
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Decides whether a cross-vendor review should be triggered, given the task
|
|
43
|
+
* classification, assessment signals, and the active review policy.
|
|
44
|
+
*
|
|
45
|
+
* @param classification - Task classification (tier + risk).
|
|
46
|
+
* @param assessment - Model self-assessment (confidence, escalate, needsReview).
|
|
47
|
+
* @param reviewPolicy - Policy field; `undefined` is treated as `'auto'` for
|
|
48
|
+
* backward compatibility.
|
|
49
|
+
*/
|
|
50
|
+
function shouldReview(classification, assessment, reviewPolicy) {
|
|
51
|
+
// 'off' — never auto-review.
|
|
52
|
+
if (reviewPolicy === 'off')
|
|
53
|
+
return false;
|
|
54
|
+
// 'critical-only' — review only when risk is critical.
|
|
55
|
+
if (reviewPolicy === 'critical-only') {
|
|
56
|
+
return classification.risk === 'critical';
|
|
57
|
+
}
|
|
58
|
+
// 'auto' (or undefined, treated as 'auto') — original behaviour.
|
|
40
59
|
return (classification.risk === 'high' ||
|
|
41
60
|
classification.risk === 'critical' ||
|
|
42
61
|
assessment.needsReview === true);
|
|
@@ -103,6 +122,12 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
103
122
|
let attempts = 0;
|
|
104
123
|
let totalCostUsd = 0;
|
|
105
124
|
let lastOutput = '';
|
|
125
|
+
// Compute history context once per orchestrate() call (before the loop).
|
|
126
|
+
// It is injected into the first-tier prompt to give stateless providers
|
|
127
|
+
// multi-turn context; the history does NOT grow during the loop.
|
|
128
|
+
const historyContext = deps.history !== undefined && deps.history.length > 0
|
|
129
|
+
? compactHistory(deps.history)
|
|
130
|
+
: undefined;
|
|
106
131
|
/**
|
|
107
132
|
* Track which attempt indices have already been reviewed so that re-runs
|
|
108
133
|
* (e.g. after a revise verdict) are not reviewed a second time and we
|
|
@@ -115,7 +140,7 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
115
140
|
mainLoop: while (attempts < deps.policy.maxAttempts) {
|
|
116
141
|
attempts++;
|
|
117
142
|
// --- Route for current tier ---
|
|
118
|
-
const decision = route(currentTier, available, deps.policy);
|
|
143
|
+
const decision = route(currentTier, available, deps.policy, deps.availableModels);
|
|
119
144
|
const provider = deps.providers[decision.provider];
|
|
120
145
|
if (provider === undefined) {
|
|
121
146
|
yield {
|
|
@@ -125,10 +150,10 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
125
150
|
};
|
|
126
151
|
break mainLoop;
|
|
127
152
|
}
|
|
128
|
-
// --- Build prompt (with optional reviewer feedback on IC retry) ---
|
|
153
|
+
// --- Build prompt (with optional reviewer feedback on IC retry + history context) ---
|
|
129
154
|
const prompt = currentTier === 'ic' && managerNotes !== undefined
|
|
130
|
-
? buildPrompt(currentTier, task, managerNotes)
|
|
131
|
-
: buildPrompt(currentTier, task);
|
|
155
|
+
? buildPrompt(currentTier, task, managerNotes, historyContext)
|
|
156
|
+
: buildPrompt(currentTier, task, undefined, historyContext);
|
|
132
157
|
// --- Yield tier-start ---
|
|
133
158
|
yield {
|
|
134
159
|
type: 'tier-start',
|
|
@@ -262,7 +287,27 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
262
287
|
// Guard: each attempt is reviewed at most once (prevents infinite loops).
|
|
263
288
|
// Guard: skip review if the only available reviewer is the same vendor
|
|
264
289
|
// (cross-vendor review is required; same-vendor-only → skip).
|
|
265
|
-
|
|
290
|
+
// Guard: skip review when budget is exhausted (gating new spend only).
|
|
291
|
+
if (shouldReview(classification, assessment, deps.policy.reviewPolicy) &&
|
|
292
|
+
!reviewedAttempts.has(attempts)) {
|
|
293
|
+
// Budget cap: do not start a review run if we have already spent the cap.
|
|
294
|
+
if (budgetExceeded(totalCostUsd, deps.policy.maxCostUsd)) {
|
|
295
|
+
yield {
|
|
296
|
+
type: 'notice',
|
|
297
|
+
level: 'warn',
|
|
298
|
+
message: 'cost budget reached — accepting best result so far',
|
|
299
|
+
};
|
|
300
|
+
yield {
|
|
301
|
+
type: 'final',
|
|
302
|
+
success: true,
|
|
303
|
+
output: lastOutput,
|
|
304
|
+
tier: currentTier,
|
|
305
|
+
totalCostUsd,
|
|
306
|
+
sessionId: deps.session.id,
|
|
307
|
+
attempts,
|
|
308
|
+
};
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
266
311
|
const reviewerId = pickReviewer(available, decision.provider);
|
|
267
312
|
// Only proceed with a DIFFERENT-vendor reviewer (cross-vendor requirement).
|
|
268
313
|
const reviewerProvider = reviewerId !== null && reviewerId !== decision.provider
|
|
@@ -276,7 +321,7 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
276
321
|
message: `Review by ${reviewerId} (cross-vendor)`,
|
|
277
322
|
};
|
|
278
323
|
// Route reviewer at manager tier
|
|
279
|
-
const reviewDecision = route('manager', [reviewerId], deps.policy);
|
|
324
|
+
const reviewDecision = route('manager', [reviewerId], deps.policy, deps.availableModels);
|
|
280
325
|
const reviewPrompt = buildReviewPrompt(task, lastOutput);
|
|
281
326
|
// Yield tier-start for review run
|
|
282
327
|
yield {
|
|
@@ -432,6 +477,24 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
432
477
|
return;
|
|
433
478
|
}
|
|
434
479
|
if (verdict.verdict === 'revise') {
|
|
480
|
+
// Budget cap: do not retry current tier if we have spent the cap.
|
|
481
|
+
if (budgetExceeded(totalCostUsd, deps.policy.maxCostUsd)) {
|
|
482
|
+
yield {
|
|
483
|
+
type: 'notice',
|
|
484
|
+
level: 'warn',
|
|
485
|
+
message: 'cost budget reached — accepting best result so far',
|
|
486
|
+
};
|
|
487
|
+
yield {
|
|
488
|
+
type: 'final',
|
|
489
|
+
success: true,
|
|
490
|
+
output: lastOutput,
|
|
491
|
+
tier: currentTier,
|
|
492
|
+
totalCostUsd,
|
|
493
|
+
sessionId: deps.session.id,
|
|
494
|
+
attempts,
|
|
495
|
+
};
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
435
498
|
// Retry current tier with reviewer's notes
|
|
436
499
|
managerNotes = verdict.notes;
|
|
437
500
|
continue mainLoop;
|
|
@@ -439,6 +502,24 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
439
502
|
// verdict === 'escalate'
|
|
440
503
|
const escalateTo = nextTierUp(currentTier);
|
|
441
504
|
if (escalateTo !== null) {
|
|
505
|
+
// Budget cap: do not escalate to a new tier if we have spent the cap.
|
|
506
|
+
if (budgetExceeded(totalCostUsd, deps.policy.maxCostUsd)) {
|
|
507
|
+
yield {
|
|
508
|
+
type: 'notice',
|
|
509
|
+
level: 'warn',
|
|
510
|
+
message: 'cost budget reached — accepting best result so far',
|
|
511
|
+
};
|
|
512
|
+
yield {
|
|
513
|
+
type: 'final',
|
|
514
|
+
success: true,
|
|
515
|
+
output: lastOutput,
|
|
516
|
+
tier: currentTier,
|
|
517
|
+
totalCostUsd,
|
|
518
|
+
sessionId: deps.session.id,
|
|
519
|
+
attempts,
|
|
520
|
+
};
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
442
523
|
yield { type: 'escalate', from: currentTier, to: escalateTo, reason: 'reviewer escalation' };
|
|
443
524
|
currentTier = escalateTo;
|
|
444
525
|
}
|
|
@@ -452,6 +533,24 @@ export async function* orchestrate(task, deps, signal) {
|
|
|
452
533
|
(assessment.confidence !== null && assessment.confidence < threshold);
|
|
453
534
|
const nextTier = nextTierUp(currentTier);
|
|
454
535
|
if (needEsc && nextTier !== null) {
|
|
536
|
+
// Budget cap: do not escalate to a new tier if we have spent the cap.
|
|
537
|
+
if (budgetExceeded(totalCostUsd, deps.policy.maxCostUsd)) {
|
|
538
|
+
yield {
|
|
539
|
+
type: 'notice',
|
|
540
|
+
level: 'warn',
|
|
541
|
+
message: 'cost budget reached — accepting best result so far',
|
|
542
|
+
};
|
|
543
|
+
yield {
|
|
544
|
+
type: 'final',
|
|
545
|
+
success: true,
|
|
546
|
+
output: lastOutput,
|
|
547
|
+
tier: currentTier,
|
|
548
|
+
totalCostUsd,
|
|
549
|
+
sessionId: deps.session.id,
|
|
550
|
+
attempts,
|
|
551
|
+
};
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
455
554
|
const escalateReason = assessment.reason !== 'model provided no reason' &&
|
|
456
555
|
assessment.reason !== 'no confidence envelope'
|
|
457
556
|
? assessment.reason
|