muonroi-cli 1.6.0 → 1.6.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/dist/src/cli/cost-forensics.d.ts +3 -0
- package/dist/src/cli/cost-forensics.js +11 -0
- package/dist/src/cli/cost-forensics.test.js +1 -0
- package/dist/src/cli/experience-report.d.ts +20 -0
- package/dist/src/cli/experience-report.js +76 -0
- package/dist/src/cli/experience-report.test.d.ts +5 -0
- package/dist/src/cli/experience-report.test.js +63 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/__tests__/directives.test.js +24 -1
- package/dist/src/gsd/directives.d.ts +22 -0
- package/dist/src/gsd/directives.js +34 -10
- package/dist/src/index.js +9 -0
- package/dist/src/mcp/__tests__/client-pool.spec.js +54 -4
- package/dist/src/mcp/__tests__/forensics-tools.test.js +1 -0
- package/dist/src/mcp/client-pool.d.ts +9 -2
- package/dist/src/mcp/client-pool.js +60 -21
- package/dist/src/orchestrator/message-processor.js +34 -2
- package/dist/src/orchestrator/session-experience.d.ts +89 -0
- package/dist/src/orchestrator/session-experience.js +169 -0
- package/dist/src/orchestrator/session-experience.test.d.ts +6 -0
- package/dist/src/orchestrator/session-experience.test.js +72 -0
- package/dist/src/orchestrator/stream-runner.js +4 -0
- package/dist/src/pil/__tests__/layer3-ee-injection.test.js +5 -3
- package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +31 -0
- package/dist/src/pil/__tests__/pipeline.test.js +17 -0
- package/dist/src/pil/layer3-ee-injection.d.ts +9 -0
- package/dist/src/pil/layer3-ee-injection.js +29 -0
- package/dist/src/pil/layer4-gsd.js +3 -2
- package/dist/src/pil/pipeline.js +11 -0
- package/dist/src/pil/session-experience-injection.d.ts +34 -0
- package/dist/src/pil/session-experience-injection.js +54 -0
- package/dist/src/pil/session-experience-injection.test.d.ts +6 -0
- package/dist/src/pil/session-experience-injection.test.js +79 -0
- package/dist/src/storage/interaction-log.d.ts +1 -1
- package/dist/src/storage/interaction-log.js +17 -4
- package/dist/src/storage/session-experience-store.d.ts +63 -0
- package/dist/src/storage/session-experience-store.js +164 -0
- package/dist/src/storage/session-experience-store.test.d.ts +5 -0
- package/dist/src/storage/session-experience-store.test.js +86 -0
- package/dist/src/storage/ui-interaction-log.js +4 -2
- package/dist/src/tools/registry-ee-query.test.js +7 -1
- package/dist/src/tools/registry.js +7 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/ui/app.js +0 -0
- package/package.json +1 -1
|
@@ -66,6 +66,7 @@ import { getModelInfo } from "../models/registry.js";
|
|
|
66
66
|
import { cheapModelShellLine, injectCheapModelPlaybook, injectCheapModelShellDirective, shouldInjectCheapModelPlaybook, } from "../pil/cheap-model-playbook.js";
|
|
67
67
|
import { injectCheapModelWorkbook, shouldInjectCheapModelWorkbook } from "../pil/cheap-model-workbooks.js";
|
|
68
68
|
import { applyPilSuffix, getResponseTaskType, getResponseToolSet, isResponseTool, runPipeline, shouldHaltOnResponseTool, } from "../pil/index.js";
|
|
69
|
+
import { isMetaAnalysisPrompt } from "../pil/layer6-output.js";
|
|
69
70
|
import { taskTypeToMaxTokens, taskTypeToReasoningEffort, taskTypeToTier } from "../pil/task-tier-map.js";
|
|
70
71
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
71
72
|
import { loadKeyForProvider } from "../providers/keychain.js";
|
|
@@ -78,6 +79,7 @@ import { reportRouteOutcome } from "../router/decide.js";
|
|
|
78
79
|
import { decideStepRouting, getStepRouterConfig } from "../router/step-router.js";
|
|
79
80
|
import { routerStore } from "../router/store.js";
|
|
80
81
|
import { getNextMessageSequence, logInteraction, markMessageErrored, markToolCallErrored, persistMessageWriteAhead, persistToolCallWriteAhead, } from "../storage/index.js";
|
|
82
|
+
import { persistSessionExperience } from "../storage/session-experience-store.js";
|
|
81
83
|
import { createBuiltinTools } from "../tools/registry.js";
|
|
82
84
|
import { snapshotFromTodoWriteArgs } from "../tools/todo-write-snapshot.js";
|
|
83
85
|
import { visionToolsNeeded } from "../tools/vision-gate.js";
|
|
@@ -103,6 +105,7 @@ import { buildRepetitionReminder, recordAssistantBurst, shouldInjectRepetitionRe
|
|
|
103
105
|
import { classifyStreamError } from "./retry-classifier.js";
|
|
104
106
|
import { forcedFinalize, getSessionLastTask, incSessionStep, parseBudgetOverride, recordSessionLastTask, resetSessionStep, resolveCeiling, } from "./scope-ceiling.js";
|
|
105
107
|
import { attachReminderToMessages, buildCheckpointReminder, buildScopeReminder, cadenceForSize, shouldInjectCeilingCrossing, shouldInjectReminder, shouldInjectSoftWarn, shouldPreWarnCompaction, } from "./scope-reminder.js";
|
|
108
|
+
import { formatElisionManifest, getSessionExperienceCounts, recordCompaction, recordElision, } from "./session-experience.js";
|
|
106
109
|
import { attemptStallRescue, pushStallToolResult } from "./stall-rescue.js";
|
|
107
110
|
import { createStallWatchdog, STALL_ERROR_MESSAGE } from "./stall-watchdog.js";
|
|
108
111
|
import { wrapToolSetWithCap } from "./sub-agent-cap.js";
|
|
@@ -1505,6 +1508,10 @@ export class MessageProcessor {
|
|
|
1505
1508
|
// rehydrate it even if EE is down (the EE extract below caps at 8k
|
|
1506
1509
|
// and needs the network; the cache keeps up to 200k, no network).
|
|
1507
1510
|
recordArtifact(toolCallId, toolName, fullContent);
|
|
1511
|
+
// Lived-experience telemetry: count this elision so a later
|
|
1512
|
+
// "cảm nhận trong CLI" question answers from data, and so the
|
|
1513
|
+
// post-compaction note can list what it just stubbed.
|
|
1514
|
+
recordElision(toolCallId, toolName, fullContent.length, sn);
|
|
1508
1515
|
try {
|
|
1509
1516
|
getDefaultEEClient()
|
|
1510
1517
|
.extract({
|
|
@@ -1527,13 +1534,20 @@ export class MessageProcessor {
|
|
|
1527
1534
|
};
|
|
1528
1535
|
const compacted = compactSubAgentMessages(stripped, {
|
|
1529
1536
|
thresholdChars: topLevelCompactThreshold,
|
|
1530
|
-
|
|
1537
|
+
// Rec #1 (cheap part): on meta/self-eval turns keep a couple more
|
|
1538
|
+
// trailing tool turns verbatim — those carry the reasoning the
|
|
1539
|
+
// agent is being asked to reflect on, and over-eliding them is
|
|
1540
|
+
// exactly what starves a self-evaluation. One boolean, no new
|
|
1541
|
+
// detection logic (isMetaAnalysisPrompt already gates layer3/5).
|
|
1542
|
+
keepLastTurns: topLevelCompactKeepLast + (isMetaAnalysisPrompt(userMessage) ? 2 : 0),
|
|
1531
1543
|
label: "top-level",
|
|
1532
1544
|
envelopeChars,
|
|
1533
1545
|
contextWindowTokens,
|
|
1534
1546
|
keepToolIds: keepToolIds.length ? keepToolIds : undefined,
|
|
1535
1547
|
persistArtifact,
|
|
1536
1548
|
});
|
|
1549
|
+
if (compacted !== stripped)
|
|
1550
|
+
recordCompaction(sn);
|
|
1537
1551
|
// Pre-compaction visibility: give the agent one step of notice
|
|
1538
1552
|
// before B4 actually rewrites history into stubs. This is the
|
|
1539
1553
|
// advance warning that was missing — agent can now decide to
|
|
@@ -1619,7 +1633,15 @@ export class MessageProcessor {
|
|
|
1619
1633
|
// "task finished?", "compacted yet?", "EE checkpoint" so agent can self-assess and avoid mù
|
|
1620
1634
|
// even when the top-level summary is not in its immediate focus (sub-agents, long loops).
|
|
1621
1635
|
const _compactNote = compacted !== stripped
|
|
1622
|
-
?
|
|
1636
|
+
? (() => {
|
|
1637
|
+
// Rec #2: turn the generic "high-value elided? use ee_query"
|
|
1638
|
+
// prose into a concrete, actionable manifest of what was just
|
|
1639
|
+
// stubbed (id/tool/size) — sourced from the elisions recorded
|
|
1640
|
+
// by persistArtifact above — so the rehydrate round-trip is
|
|
1641
|
+
// informed, not blind.
|
|
1642
|
+
const _m = formatElisionManifest();
|
|
1643
|
+
return `[context compacted at step ${sn} — older or low-value tool results rewritten to stubs to fit budget. High-value evidence (file reads, bash, your previous responses) is kept verbatim. ${buildCheckpointReminder(sn, true)}${_m ? ` ${_m}` : ""}]`;
|
|
1644
|
+
})()
|
|
1623
1645
|
: null;
|
|
1624
1646
|
if (_compactNote) {
|
|
1625
1647
|
return { messages: attachReminderToMessages(compacted, _compactNote) };
|
|
@@ -1680,6 +1702,16 @@ export class MessageProcessor {
|
|
|
1680
1702
|
console.error("[Agent:onFinish] failed to emit llm-done", err);
|
|
1681
1703
|
}
|
|
1682
1704
|
deps.setCurrentCallId("");
|
|
1705
|
+
// Rec #1 persisted forensics: onFinish fires once per top-level turn,
|
|
1706
|
+
// so flush this session's cumulative experience counts here. Readers
|
|
1707
|
+
// take the latest row per session, so the last turn's row is the
|
|
1708
|
+
// session total. No-ops on missing id / all-zero. Fail-open.
|
|
1709
|
+
try {
|
|
1710
|
+
persistSessionExperience(deps.session?.id ?? null, getSessionExperienceCounts());
|
|
1711
|
+
}
|
|
1712
|
+
catch (err) {
|
|
1713
|
+
console.error("[Agent:onFinish] persistSessionExperience failed", err);
|
|
1714
|
+
}
|
|
1683
1715
|
},
|
|
1684
1716
|
});
|
|
1685
1717
|
let _topTokenIndex = 0;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/orchestrator/session-experience.ts
|
|
3
|
+
*
|
|
4
|
+
* In-process record of what actually happened to the agent in THIS CLI session:
|
|
5
|
+
* how often compaction fired, which tool outputs were elided, how many of those
|
|
6
|
+
* the agent rehydrated via ee_query (and from where), and whether the Experience
|
|
7
|
+
* Engine misbehaved. It is the single source of truth for the agent's *lived*
|
|
8
|
+
* session experience.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: when a user asks "cảm nhận trong CLI" / "do you feel blind in
|
|
11
|
+
* this session", the agent used to answer by READING the anti-mù source code and
|
|
12
|
+
* theorizing about mechanisms (session ce816796a57d) — friction it never actually
|
|
13
|
+
* observed. That is backwards. With this tracker the agent answers from data —
|
|
14
|
+
* "compaction fired 3x, I rehydrated 2 artifacts, never lost context" — instead of
|
|
15
|
+
* inferring from code. The same counters double as the "measure before you
|
|
16
|
+
* re-architect" instrumentation: how often a real session actually elides a stub
|
|
17
|
+
* the agent then needs.
|
|
18
|
+
*
|
|
19
|
+
* Process-scoped singleton == session-scoped: one CLI invocation is one session.
|
|
20
|
+
* Pure module, no I/O, fully unit-testable; reset hook for tests.
|
|
21
|
+
*/
|
|
22
|
+
export type RehydrateSource = "cache" | "disk" | "ee" | "unavailable";
|
|
23
|
+
export interface ElisionRecord {
|
|
24
|
+
toolCallId: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
/** Full length of the elided output, in chars. */
|
|
27
|
+
chars: number;
|
|
28
|
+
/** prepareStep step number at which it was elided. */
|
|
29
|
+
step: number;
|
|
30
|
+
}
|
|
31
|
+
export interface SessionExperience {
|
|
32
|
+
compactions: number;
|
|
33
|
+
lastCompactionStep: number | null;
|
|
34
|
+
elisions: ReadonlyArray<ElisionRecord>;
|
|
35
|
+
totalElidedChars: number;
|
|
36
|
+
rehydrations: Readonly<Record<RehydrateSource, number>>;
|
|
37
|
+
eeTimeouts: number;
|
|
38
|
+
eeErrors: number;
|
|
39
|
+
}
|
|
40
|
+
/** Record that B3/B4 compaction actually elided something at `step`. */
|
|
41
|
+
export declare function recordCompaction(step: number): void;
|
|
42
|
+
/** Record a single tool output the compactor rewrote into a stub. */
|
|
43
|
+
export declare function recordElision(toolCallId: string, toolName: string, chars: number, step: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Record an ee_query rehydrate of an elided artifact, tagged by where it came
|
|
46
|
+
* from. `unavailable` means the agent asked for an artifact that was neither in
|
|
47
|
+
* the local cache nor recoverable from EE — the "needed-but-couldn't-get" signal.
|
|
48
|
+
*/
|
|
49
|
+
export declare function recordRehydration(source: RehydrateSource): void;
|
|
50
|
+
/** Record an Experience Engine timeout or non-timeout error felt this session. */
|
|
51
|
+
export declare function recordEeEvent(kind: "timeout" | "error"): void;
|
|
52
|
+
/**
|
|
53
|
+
* Flat scalar counts — the shape persisted per session and aggregated
|
|
54
|
+
* cross-session by `usage experience` to decide whether compaction friction is
|
|
55
|
+
* real at a painful rate (no nested arrays, JSON-stable).
|
|
56
|
+
*/
|
|
57
|
+
export interface SessionExperienceCounts {
|
|
58
|
+
compactions: number;
|
|
59
|
+
elided: number;
|
|
60
|
+
totalElidedChars: number;
|
|
61
|
+
rehydratedCache: number;
|
|
62
|
+
rehydratedDisk: number;
|
|
63
|
+
rehydratedEe: number;
|
|
64
|
+
unavailable: number;
|
|
65
|
+
eeTimeouts: number;
|
|
66
|
+
eeErrors: number;
|
|
67
|
+
}
|
|
68
|
+
/** Scalar counts for persistence/aggregation (drops the per-elision array). */
|
|
69
|
+
export declare function getSessionExperienceCounts(): SessionExperienceCounts;
|
|
70
|
+
/** Immutable snapshot of the session so far. */
|
|
71
|
+
export declare function getSessionExperience(): SessionExperience;
|
|
72
|
+
/** Most-recent elisions, newest first — feeds the checkpoint manifest. */
|
|
73
|
+
export declare function recentElisions(n?: number): ElisionRecord[];
|
|
74
|
+
/** True when literally nothing notable has happened yet (context intact). */
|
|
75
|
+
export declare function isSessionExperienceEmpty(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* A compact manifest of the most-recently elided tool outputs, for the
|
|
78
|
+
* post-compaction checkpoint note: turns the generic "high-value elided? use
|
|
79
|
+
* ee_query" prose into a concrete, actionable list so the agent's rehydrate
|
|
80
|
+
* round-trip is informed rather than blind.
|
|
81
|
+
*/
|
|
82
|
+
export declare function formatElisionManifest(n?: number): string;
|
|
83
|
+
/**
|
|
84
|
+
* The agent-facing felt summary. Injected when the user asks how the agent is
|
|
85
|
+
* doing IN this session, so the answer is grounded in what actually happened —
|
|
86
|
+
* not in a fresh reading of the compaction/PIL source.
|
|
87
|
+
*/
|
|
88
|
+
export declare function formatSessionExperience(): string;
|
|
89
|
+
export declare function __resetSessionExperienceForTests(): void;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/orchestrator/session-experience.ts
|
|
3
|
+
*
|
|
4
|
+
* In-process record of what actually happened to the agent in THIS CLI session:
|
|
5
|
+
* how often compaction fired, which tool outputs were elided, how many of those
|
|
6
|
+
* the agent rehydrated via ee_query (and from where), and whether the Experience
|
|
7
|
+
* Engine misbehaved. It is the single source of truth for the agent's *lived*
|
|
8
|
+
* session experience.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: when a user asks "cảm nhận trong CLI" / "do you feel blind in
|
|
11
|
+
* this session", the agent used to answer by READING the anti-mù source code and
|
|
12
|
+
* theorizing about mechanisms (session ce816796a57d) — friction it never actually
|
|
13
|
+
* observed. That is backwards. With this tracker the agent answers from data —
|
|
14
|
+
* "compaction fired 3x, I rehydrated 2 artifacts, never lost context" — instead of
|
|
15
|
+
* inferring from code. The same counters double as the "measure before you
|
|
16
|
+
* re-architect" instrumentation: how often a real session actually elides a stub
|
|
17
|
+
* the agent then needs.
|
|
18
|
+
*
|
|
19
|
+
* Process-scoped singleton == session-scoped: one CLI invocation is one session.
|
|
20
|
+
* Pure module, no I/O, fully unit-testable; reset hook for tests.
|
|
21
|
+
*/
|
|
22
|
+
/** Bound the elision log so a pathological session can't grow it unbounded. */
|
|
23
|
+
const MAX_ELISIONS = 200;
|
|
24
|
+
function freshState() {
|
|
25
|
+
return {
|
|
26
|
+
compactions: 0,
|
|
27
|
+
lastCompactionStep: null,
|
|
28
|
+
elisions: [],
|
|
29
|
+
rehydrations: { cache: 0, disk: 0, ee: 0, unavailable: 0 },
|
|
30
|
+
eeTimeouts: 0,
|
|
31
|
+
eeErrors: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let state = freshState();
|
|
35
|
+
/** Record that B3/B4 compaction actually elided something at `step`. */
|
|
36
|
+
export function recordCompaction(step) {
|
|
37
|
+
state.compactions += 1;
|
|
38
|
+
state.lastCompactionStep = Number.isFinite(step) ? step : state.lastCompactionStep;
|
|
39
|
+
}
|
|
40
|
+
/** Record a single tool output the compactor rewrote into a stub. */
|
|
41
|
+
export function recordElision(toolCallId, toolName, chars, step) {
|
|
42
|
+
if (!toolCallId)
|
|
43
|
+
return;
|
|
44
|
+
state.elisions.push({
|
|
45
|
+
toolCallId,
|
|
46
|
+
toolName: toolName || "",
|
|
47
|
+
chars: Number.isFinite(chars) && chars > 0 ? Math.floor(chars) : 0,
|
|
48
|
+
step: Number.isFinite(step) ? step : 0,
|
|
49
|
+
});
|
|
50
|
+
// FIFO trim — keep the most recent MAX_ELISIONS.
|
|
51
|
+
if (state.elisions.length > MAX_ELISIONS) {
|
|
52
|
+
state.elisions.splice(0, state.elisions.length - MAX_ELISIONS);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Record an ee_query rehydrate of an elided artifact, tagged by where it came
|
|
57
|
+
* from. `unavailable` means the agent asked for an artifact that was neither in
|
|
58
|
+
* the local cache nor recoverable from EE — the "needed-but-couldn't-get" signal.
|
|
59
|
+
*/
|
|
60
|
+
export function recordRehydration(source) {
|
|
61
|
+
if (source in state.rehydrations)
|
|
62
|
+
state.rehydrations[source] += 1;
|
|
63
|
+
}
|
|
64
|
+
/** Record an Experience Engine timeout or non-timeout error felt this session. */
|
|
65
|
+
export function recordEeEvent(kind) {
|
|
66
|
+
if (kind === "timeout")
|
|
67
|
+
state.eeTimeouts += 1;
|
|
68
|
+
else
|
|
69
|
+
state.eeErrors += 1;
|
|
70
|
+
}
|
|
71
|
+
/** Scalar counts for persistence/aggregation (drops the per-elision array). */
|
|
72
|
+
export function getSessionExperienceCounts() {
|
|
73
|
+
const s = getSessionExperience();
|
|
74
|
+
return {
|
|
75
|
+
compactions: s.compactions,
|
|
76
|
+
elided: s.elisions.length,
|
|
77
|
+
totalElidedChars: s.totalElidedChars,
|
|
78
|
+
rehydratedCache: s.rehydrations.cache,
|
|
79
|
+
rehydratedDisk: s.rehydrations.disk,
|
|
80
|
+
rehydratedEe: s.rehydrations.ee,
|
|
81
|
+
unavailable: s.rehydrations.unavailable,
|
|
82
|
+
eeTimeouts: s.eeTimeouts,
|
|
83
|
+
eeErrors: s.eeErrors,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Immutable snapshot of the session so far. */
|
|
87
|
+
export function getSessionExperience() {
|
|
88
|
+
return {
|
|
89
|
+
compactions: state.compactions,
|
|
90
|
+
lastCompactionStep: state.lastCompactionStep,
|
|
91
|
+
elisions: state.elisions.slice(),
|
|
92
|
+
totalElidedChars: state.elisions.reduce((sum, e) => sum + e.chars, 0),
|
|
93
|
+
rehydrations: { ...state.rehydrations },
|
|
94
|
+
eeTimeouts: state.eeTimeouts,
|
|
95
|
+
eeErrors: state.eeErrors,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Most-recent elisions, newest first — feeds the checkpoint manifest. */
|
|
99
|
+
export function recentElisions(n = 5) {
|
|
100
|
+
const take = Math.max(0, Math.floor(n));
|
|
101
|
+
return state.elisions.slice(-take).reverse();
|
|
102
|
+
}
|
|
103
|
+
/** True when literally nothing notable has happened yet (context intact). */
|
|
104
|
+
export function isSessionExperienceEmpty() {
|
|
105
|
+
return (state.compactions === 0 &&
|
|
106
|
+
state.elisions.length === 0 &&
|
|
107
|
+
state.eeTimeouts === 0 &&
|
|
108
|
+
state.eeErrors === 0 &&
|
|
109
|
+
state.rehydrations.cache === 0 &&
|
|
110
|
+
state.rehydrations.disk === 0 &&
|
|
111
|
+
state.rehydrations.ee === 0 &&
|
|
112
|
+
state.rehydrations.unavailable === 0);
|
|
113
|
+
}
|
|
114
|
+
function shortId(id) {
|
|
115
|
+
return id.length > 12 ? id.slice(0, 12) : id;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A compact manifest of the most-recently elided tool outputs, for the
|
|
119
|
+
* post-compaction checkpoint note: turns the generic "high-value elided? use
|
|
120
|
+
* ee_query" prose into a concrete, actionable list so the agent's rehydrate
|
|
121
|
+
* round-trip is informed rather than blind.
|
|
122
|
+
*/
|
|
123
|
+
export function formatElisionManifest(n = 5) {
|
|
124
|
+
const recent = recentElisions(n);
|
|
125
|
+
if (recent.length === 0)
|
|
126
|
+
return "";
|
|
127
|
+
const items = recent.map((e) => `id=${shortId(e.toolCallId)} ${e.toolName || "tool"} (${e.chars}c)`).join(" · ");
|
|
128
|
+
return `Elided this turn: ${items}. ee_query "tool-artifact id=XXX" to rehydrate the one you need.`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The agent-facing felt summary. Injected when the user asks how the agent is
|
|
132
|
+
* doing IN this session, so the answer is grounded in what actually happened —
|
|
133
|
+
* not in a fresh reading of the compaction/PIL source.
|
|
134
|
+
*/
|
|
135
|
+
export function formatSessionExperience() {
|
|
136
|
+
const s = getSessionExperience();
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push("[session experience — what ACTUALLY happened to you in THIS CLI session so far]");
|
|
139
|
+
if (isSessionExperienceEmpty()) {
|
|
140
|
+
lines.push("- Nothing notable: no compaction, no elision, no EE failures. Your context is intact this session.");
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
lines.push(s.compactions === 0
|
|
144
|
+
? "- Compaction: not fired yet — full context retained."
|
|
145
|
+
: `- Compaction: fired ${s.compactions}x${s.lastCompactionStep !== null ? ` (last at step ${s.lastCompactionStep})` : ""}.`);
|
|
146
|
+
if (s.elisions.length === 0) {
|
|
147
|
+
lines.push("- Tool outputs elided: none — nothing was rewritten to a stub.");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const tools = [...new Set(s.elisions.map((e) => e.toolName || "tool"))].join(", ");
|
|
151
|
+
lines.push(`- Tool outputs elided: ${s.elisions.length} (${s.totalElidedChars} chars; via ${tools}).`);
|
|
152
|
+
}
|
|
153
|
+
const r = s.rehydrations;
|
|
154
|
+
const rehydratedTotal = r.cache + r.disk + r.ee;
|
|
155
|
+
lines.push(rehydratedTotal === 0 && r.unavailable === 0
|
|
156
|
+
? "- Rehydrated via ee_query: none requested."
|
|
157
|
+
: `- Rehydrated via ee_query: cache=${r.cache} disk=${r.disk} ee=${r.ee}; needed-but-unavailable=${r.unavailable}.`);
|
|
158
|
+
if (s.eeTimeouts > 0 || s.eeErrors > 0) {
|
|
159
|
+
lines.push(`- Experience Engine: timeouts=${s.eeTimeouts} errors=${s.eeErrors}.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
lines.push("Answer the user's how-does-it-feel / are-you-blind question FROM THIS lived data — not by reading the CLI source. If everything is zero, say so plainly: nothing degraded your context this session.");
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
// ─── Test hook ─────────────────────────────────────────────────────────────
|
|
166
|
+
export function __resetSessionExperienceForTests() {
|
|
167
|
+
state = freshState();
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=session-experience.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-experience — in-process record of the agent's lived session, so a
|
|
3
|
+
* "cảm nhận trong CLI" / "are you blind?" question is answered from data, not by
|
|
4
|
+
* re-reading source. Also the "measure before re-architecting" instrumentation.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { __resetSessionExperienceForTests, formatElisionManifest, formatSessionExperience, getSessionExperience, isSessionExperienceEmpty, recentElisions, recordCompaction, recordEeEvent, recordElision, recordRehydration, } from "./session-experience.js";
|
|
8
|
+
describe("session-experience tracker", () => {
|
|
9
|
+
afterEach(() => __resetSessionExperienceForTests());
|
|
10
|
+
it("starts empty and reports an intact-context felt summary", () => {
|
|
11
|
+
expect(isSessionExperienceEmpty()).toBe(true);
|
|
12
|
+
const text = formatSessionExperience();
|
|
13
|
+
expect(text).toContain("Nothing notable");
|
|
14
|
+
expect(text).toContain("context is intact this session");
|
|
15
|
+
// Steering line must always tell the agent to use lived data, not source.
|
|
16
|
+
expect(text).toMatch(/not by reading the CLI source/i);
|
|
17
|
+
});
|
|
18
|
+
it("accumulates compaction, elision, rehydration and EE counters", () => {
|
|
19
|
+
recordCompaction(4);
|
|
20
|
+
recordCompaction(9);
|
|
21
|
+
recordElision("call_a", "read_file", 4100, 4);
|
|
22
|
+
recordElision("call_b", "grep", 2300, 9);
|
|
23
|
+
recordRehydration("cache");
|
|
24
|
+
recordRehydration("unavailable");
|
|
25
|
+
recordEeEvent("timeout");
|
|
26
|
+
const s = getSessionExperience();
|
|
27
|
+
expect(s.compactions).toBe(2);
|
|
28
|
+
expect(s.lastCompactionStep).toBe(9);
|
|
29
|
+
expect(s.elisions).toHaveLength(2);
|
|
30
|
+
expect(s.totalElidedChars).toBe(6400);
|
|
31
|
+
expect(s.rehydrations.cache).toBe(1);
|
|
32
|
+
expect(s.rehydrations.unavailable).toBe(1);
|
|
33
|
+
expect(s.eeTimeouts).toBe(1);
|
|
34
|
+
expect(isSessionExperienceEmpty()).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it("felt summary reflects real counters when non-empty", () => {
|
|
37
|
+
recordCompaction(3);
|
|
38
|
+
recordElision("call_x", "read_file", 5000, 3);
|
|
39
|
+
recordRehydration("ee");
|
|
40
|
+
const text = formatSessionExperience();
|
|
41
|
+
expect(text).toContain("fired 1x");
|
|
42
|
+
expect(text).toContain("last at step 3");
|
|
43
|
+
expect(text).toContain("Tool outputs elided: 1");
|
|
44
|
+
expect(text).toContain("ee=1");
|
|
45
|
+
expect(text).not.toContain("Nothing notable");
|
|
46
|
+
});
|
|
47
|
+
it("recentElisions returns newest first and respects the cap arg", () => {
|
|
48
|
+
for (let i = 0; i < 6; i++)
|
|
49
|
+
recordElision(`call_${i}`, "read_file", 1000 + i, i);
|
|
50
|
+
const recent = recentElisions(3);
|
|
51
|
+
expect(recent.map((e) => e.toolCallId)).toEqual(["call_5", "call_4", "call_3"]);
|
|
52
|
+
});
|
|
53
|
+
it("formatElisionManifest is empty with no elisions, actionable otherwise", () => {
|
|
54
|
+
expect(formatElisionManifest()).toBe("");
|
|
55
|
+
recordElision("0123456789abcdefXYZ", "read_file", 4096, 7);
|
|
56
|
+
const m = formatElisionManifest();
|
|
57
|
+
// id is shortened, tool + char count present, and points at ee_query.
|
|
58
|
+
expect(m).toContain("id=0123456789ab");
|
|
59
|
+
expect(m).toContain("read_file (4096c)");
|
|
60
|
+
expect(m).toMatch(/ee_query "tool-artifact id=XXX"/);
|
|
61
|
+
});
|
|
62
|
+
it("caps the elision log at 200 (FIFO) without unbounded growth", () => {
|
|
63
|
+
for (let i = 0; i < 250; i++)
|
|
64
|
+
recordElision(`c_${i}`, "bash", 500, i);
|
|
65
|
+
const s = getSessionExperience();
|
|
66
|
+
expect(s.elisions).toHaveLength(200);
|
|
67
|
+
// Oldest 50 dropped; newest retained.
|
|
68
|
+
expect(s.elisions[0].toolCallId).toBe("c_50");
|
|
69
|
+
expect(s.elisions.at(-1).toolCallId).toBe("c_249");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=session-experience.test.js.map
|
|
@@ -53,6 +53,7 @@ import { repairToolCallHook } from "./repair-tool-call.js";
|
|
|
53
53
|
import { classifyStreamError } from "./retry-classifier.js";
|
|
54
54
|
import { incSessionStep, resolveCeiling } from "./scope-ceiling.js";
|
|
55
55
|
import { attachReminderToMessages, buildScopeReminder, cadenceForSize, shouldInjectReminder, shouldInjectSoftWarn, } from "./scope-reminder.js";
|
|
56
|
+
import { recordCompaction, recordElision } from "./session-experience.js";
|
|
56
57
|
import { createStallWatchdog, STALL_ERROR_MESSAGE } from "./stall-watchdog.js";
|
|
57
58
|
import { wrapToolSetWithCap } from "./sub-agent-cap.js";
|
|
58
59
|
import { compactSubAgentMessages } from "./subagent-compactor.js";
|
|
@@ -415,6 +416,7 @@ export class StreamRunner {
|
|
|
415
416
|
const persistSubArtifact = (toolCallId, toolName, fullContent, reason) => {
|
|
416
417
|
// Local-first durable cache so ee_query rehydrates even when EE is down.
|
|
417
418
|
recordArtifact(toolCallId, toolName, fullContent);
|
|
419
|
+
recordElision(toolCallId, toolName, fullContent.length, stepNumber);
|
|
418
420
|
try {
|
|
419
421
|
getDefaultEEClient()
|
|
420
422
|
.extract({
|
|
@@ -435,6 +437,8 @@ export class StreamRunner {
|
|
|
435
437
|
keepToolIds: subKeepToolIds.length ? subKeepToolIds : undefined,
|
|
436
438
|
persistArtifact: persistSubArtifact,
|
|
437
439
|
});
|
|
440
|
+
if (compacted !== stripped)
|
|
441
|
+
recordCompaction(stepNumber);
|
|
438
442
|
// Phase 4A — scope reminder injection for the sub-agent loop.
|
|
439
443
|
// Mirror of the top-level wiring in message-processor.ts:
|
|
440
444
|
// K = cadenceForSize(size) where size defaults to "medium" because
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
-
import { layer3EeInjection } from "../layer3-ee-injection.js";
|
|
2
|
+
import { layer3EeInjection, RECALL_FEEDBACK_NUDGE } from "../layer3-ee-injection.js";
|
|
3
3
|
vi.mock("../../ee/bridge.js", () => ({
|
|
4
4
|
searchByText: vi.fn().mockResolvedValue([]),
|
|
5
5
|
}));
|
|
@@ -93,8 +93,10 @@ describe("layer3EeInjection (bridge-based)", () => {
|
|
|
93
93
|
const chars = parseInt(charsMatch[1], 10);
|
|
94
94
|
// Two parallel collections, each at 15% of budget: 15% of 100 tokens * 4 chars/token
|
|
95
95
|
// = 60 chars per block + 3 for "..." suffix, joined with newline. Allow generous
|
|
96
|
-
// ceiling for header text + 2 blocks
|
|
97
|
-
|
|
96
|
+
// ceiling for header text + 2 blocks, PLUS the fixed ee_feedback nudge appended
|
|
97
|
+
// when rateable experience is present. The 2000-char input means a truncation
|
|
98
|
+
// regression would blow well past this bound regardless.
|
|
99
|
+
expect(chars).toBeLessThanOrEqual(260 + RECALL_FEEDBACK_NUDGE.length + 1);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
});
|
|
@@ -75,6 +75,37 @@ describe("layer3 experience_injected chunk emission (CQ-16b)", () => {
|
|
|
75
75
|
expect(chunk?.experienceInjected?.scoreFloor).toBeDefined();
|
|
76
76
|
expect(typeof chunk?.experienceInjected?.scoreFloor).toBe("number");
|
|
77
77
|
});
|
|
78
|
+
it("experience_injected chunk carries per-point {id, title, tier} so the TUI can show WHAT was injected", async () => {
|
|
79
|
+
mockSearchByText.mockResolvedValue([
|
|
80
|
+
{
|
|
81
|
+
id: "point-1",
|
|
82
|
+
score: 0.9,
|
|
83
|
+
payload: { text: "Use dependency injection for testability" },
|
|
84
|
+
collection: "experience-behavioral",
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
await layer3EeInjection(BASE_CTX);
|
|
88
|
+
const chunk = capturedSinkCalls.find((c) => typeof c !== "string" && c.type === "experience_injected");
|
|
89
|
+
const points = chunk?.experienceInjected?.points;
|
|
90
|
+
expect(Array.isArray(points)).toBe(true);
|
|
91
|
+
expect(points.length).toBeGreaterThan(0);
|
|
92
|
+
const p = points[0];
|
|
93
|
+
expect(p.id).toBe("point-1");
|
|
94
|
+
expect(p.title).toContain("dependency injection");
|
|
95
|
+
expect(["principle", "behavioral", "checkpoint"]).toContain(p.tier);
|
|
96
|
+
});
|
|
97
|
+
it("appends an ee_feedback nudge to the injected text when rateable experience is present", async () => {
|
|
98
|
+
mockSearchByText.mockResolvedValue([
|
|
99
|
+
{
|
|
100
|
+
id: "p1",
|
|
101
|
+
score: 0.9,
|
|
102
|
+
payload: { text: "Prefer composition over inheritance" },
|
|
103
|
+
collection: "experience-behavioral",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
const result = await layer3EeInjection(BASE_CTX);
|
|
107
|
+
expect(result.enriched).toContain("ee_feedback(id, followed|ignored|noise)");
|
|
108
|
+
});
|
|
78
109
|
it("does NOT emit experience_injected when searchByText returns empty array", async () => {
|
|
79
110
|
mockSearchByText.mockResolvedValue([]);
|
|
80
111
|
await layer3EeInjection(BASE_CTX);
|
|
@@ -65,6 +65,23 @@ describe("runPipeline()", () => {
|
|
|
65
65
|
expect(ctx.layers[i].delta).not.toBe("skipped:null-taskType");
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
|
+
it("felt-experience prompt injects the session snapshot even when taskType is null", async () => {
|
|
69
|
+
// Regression: the felt-experience injection was first placed INSIDE the
|
|
70
|
+
// `taskType !== null` branch, so a "cảm nhận trong CLI" question that
|
|
71
|
+
// classifies to null (not a coding task) silently skipped it. It must run
|
|
72
|
+
// regardless of taskType.
|
|
73
|
+
mockClassify.mockReturnValue({ tier: "abstain", confidence: 0.2, reason: "low-confidence" });
|
|
74
|
+
const ctx = await runPipeline("bạn có bị mù context không trong session này, cảm nhận thế nào");
|
|
75
|
+
expect(ctx.taskType).toBeNull();
|
|
76
|
+
expect(ctx.layers.find((l) => l.name === "session-experience")?.applied).toBe(true);
|
|
77
|
+
expect(ctx.enriched).toContain("[session experience —");
|
|
78
|
+
expect(ctx.enriched).toMatch(/not by reading the CLI source/i);
|
|
79
|
+
});
|
|
80
|
+
it("plain evaluate-the-CLI prompt does NOT inject the session snapshot", async () => {
|
|
81
|
+
const ctx = await runPipeline("đánh giá agent bên trong cli và đề xuất cải thiện");
|
|
82
|
+
expect(ctx.layers.find((l) => l.name === "session-experience")).toBeUndefined();
|
|
83
|
+
expect(ctx.enriched).not.toContain("[session experience —");
|
|
84
|
+
});
|
|
68
85
|
it("metrics.totalMs is a non-negative number", async () => {
|
|
69
86
|
const ctx = await runPipeline("refactor this");
|
|
70
87
|
expect(ctx.metrics).not.toBeNull();
|
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
* and PIL Layer 3 are active on the same pipeline run.
|
|
16
16
|
*/
|
|
17
17
|
import type { PipelineContext } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Inline reminder appended to the injected experience block (when rateable
|
|
20
|
+
* principles/behavioral are present) so passively-injected recalls carry a
|
|
21
|
+
* feedback prompt next to their [id:..] handles — the front-loaded
|
|
22
|
+
* native-capabilities instruction can be compacted away on long sessions, and
|
|
23
|
+
* unrated recalls degrade future recall (the recall arm of the EE loop is
|
|
24
|
+
* explicit-feedback-only by design).
|
|
25
|
+
*/
|
|
26
|
+
export declare const RECALL_FEEDBACK_NUDGE = "\u21B3 Acted on one of the above [id:..]? Rate it: ee_feedback(id, followed|ignored|noise). Unrated recalls degrade future recall.";
|
|
18
27
|
export declare function layer3EeInjection(ctx: PipelineContext): Promise<PipelineContext>;
|
|
19
28
|
/**
|
|
20
29
|
* Issue #4 — meta-turn TARGETED complement to Layer 3's checkpoint arm.
|
|
@@ -45,6 +45,15 @@ const PIL_PRINCIPLES_FLOOR = Math.max(0, PIL_SCORE_FLOOR - 0.15);
|
|
|
45
45
|
// hitCount threshold for promoting a behavioral point to T1 "proven" reflex.
|
|
46
46
|
// Mirrors the EE evolution promotion rule (3 confirmed hits → T1).
|
|
47
47
|
const T1_HIT_THRESHOLD = 3;
|
|
48
|
+
/**
|
|
49
|
+
* Inline reminder appended to the injected experience block (when rateable
|
|
50
|
+
* principles/behavioral are present) so passively-injected recalls carry a
|
|
51
|
+
* feedback prompt next to their [id:..] handles — the front-loaded
|
|
52
|
+
* native-capabilities instruction can be compacted away on long sessions, and
|
|
53
|
+
* unrated recalls degrade future recall (the recall arm of the EE loop is
|
|
54
|
+
* explicit-feedback-only by design).
|
|
55
|
+
*/
|
|
56
|
+
export const RECALL_FEEDBACK_NUDGE = "↳ Acted on one of the above [id:..]? Rate it: ee_feedback(id, followed|ignored|noise). Unrated recalls degrade future recall.";
|
|
48
57
|
/**
|
|
49
58
|
* Extract all sha16 values from `<!-- bb-context-injected:<sha16> -->` markers
|
|
50
59
|
* already present in the enriched context string.
|
|
@@ -289,12 +298,22 @@ export async function layer3EeInjection(ctx) {
|
|
|
289
298
|
// STALE-01: Register injected point IDs for prompt-stale reconciliation.
|
|
290
299
|
updateLastSurfacedState(allPoints.map((p) => String(p.id)));
|
|
291
300
|
// CQ-16b: Emit experience_injected StreamChunk so TUI can show collapsible block.
|
|
301
|
+
// Carry per-point {id, title, tier} so the TUI can show WHAT was injected, not
|
|
302
|
+
// just how many (the data already exists here; previously only the count + ids
|
|
303
|
+
// reached the client and the title was never serialized).
|
|
304
|
+
const pointTitle = (p) => (extractPointText(p).split("\n")[0] ?? "").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
305
|
+
const injectedPoints = [
|
|
306
|
+
...deduplicatedPrinciples.map((p) => ({ id: String(p.id), title: pointTitle(p), tier: "principle" })),
|
|
307
|
+
...deduplicatedBehavioral.map((p) => ({ id: String(p.id), title: pointTitle(p), tier: "behavioral" })),
|
|
308
|
+
...deduplicatedCheckpoints.map((p) => ({ id: String(p.id), title: pointTitle(p), tier: "checkpoint" })),
|
|
309
|
+
];
|
|
292
310
|
try {
|
|
293
311
|
const injectedChunk = {
|
|
294
312
|
type: "experience_injected",
|
|
295
313
|
experienceInjected: {
|
|
296
314
|
pointCount: totalPoints + deduplicatedCheckpoints.length,
|
|
297
315
|
pointIds: allPoints.map((p) => String(p.id)),
|
|
316
|
+
points: injectedPoints,
|
|
298
317
|
scoreFloor: PIL_SCORE_FLOOR,
|
|
299
318
|
taskType: ctx.taskType ?? undefined,
|
|
300
319
|
domain: ctx.domain ?? undefined,
|
|
@@ -324,6 +343,16 @@ export async function layer3EeInjection(ctx) {
|
|
|
324
343
|
// Idea 5: raised from 0.08 to 0.12 for higher fidelity on critical progress + artifact refs.
|
|
325
344
|
parts.push(truncateToBudget(cpText + "\n" + marker, Math.floor(ctx.tokenBudget * 0.12)));
|
|
326
345
|
}
|
|
346
|
+
// Close the recall feedback loop at the injection site: passively-injected
|
|
347
|
+
// experience (the agent did not ee_query for it) otherwise carries no feedback
|
|
348
|
+
// prompt, so it goes unrated and EE cannot learn if the injection was gold or
|
|
349
|
+
// noise. The front-loaded native-capabilities instruction can be compacted away
|
|
350
|
+
// on long sessions; this nudge rides next to the [id:..] handles it refers to.
|
|
351
|
+
// Gated on rateable experience (principles/behavioral) — checkpoints are task
|
|
352
|
+
// artifacts, not recall verdicts.
|
|
353
|
+
if (deduplicatedPrinciples.length + deduplicatedBehavioral.length > 0) {
|
|
354
|
+
parts.push(RECALL_FEEDBACK_NUDGE);
|
|
355
|
+
}
|
|
327
356
|
const injected = parts.join("\n");
|
|
328
357
|
try {
|
|
329
358
|
if (ctx.sessionId) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { routeTask } from "../ee/bridge.js";
|
|
20
20
|
import { scoreComplexity } from "../gsd/complexity.js";
|
|
21
|
-
import { buildDirective } from "../gsd/directives.js";
|
|
21
|
+
import { buildDirective, mentionsEcosystemScope } from "../gsd/directives.js";
|
|
22
22
|
import { detectGrayAreas } from "../gsd/gray-areas.js";
|
|
23
23
|
import { detectGsdPhase } from "../gsd/types.js";
|
|
24
24
|
import { classifyEeError, logEeFailure } from "../utils/ee-logger.js";
|
|
@@ -96,7 +96,8 @@ export async function layer4Gsd(ctx) {
|
|
|
96
96
|
: isMetaAnalysisPrompt(ctx.raw) ||
|
|
97
97
|
(ctx.taskType === "general" && ctx.intentKind === "task") ||
|
|
98
98
|
(isQuestionLike(ctx.raw) && !isImplementationIntent(ctx.raw));
|
|
99
|
-
const
|
|
99
|
+
const ecosystem = mentionsEcosystemScope(ctx.raw);
|
|
100
|
+
const directive = buildDirective({ complexity, phase, grayAreas, informational, ecosystem });
|
|
100
101
|
const budgetChars = Math.floor(ctx.tokenBudget * DIRECTIVE_BUDGET_FRACTION);
|
|
101
102
|
const trimmed = truncateToBudget(directive.text, budgetChars);
|
|
102
103
|
return {
|