@alan512/experienceengine 0.1.3 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/README.md +213 -130
- package/README.zh-CN.md +250 -119
- package/dist/adapters/claude-code/session-store.d.ts +1 -0
- package/dist/adapters/claude-code/session-store.js +24 -1
- package/dist/adapters/claude-code/session-store.js.map +1 -1
- package/dist/adapters/codex/action-registry.d.ts +84 -0
- package/dist/adapters/codex/action-registry.js +277 -0
- package/dist/adapters/codex/action-registry.js.map +1 -0
- package/dist/adapters/codex/broker-tools.d.ts +114 -0
- package/dist/adapters/codex/broker-tools.js +130 -0
- package/dist/adapters/codex/broker-tools.js.map +1 -0
- package/dist/adapters/codex/mcp-server.d.ts +21 -0
- package/dist/adapters/codex/mcp-server.js +103 -423
- package/dist/adapters/codex/mcp-server.js.map +1 -1
- package/dist/analyzer/candidate-signals.d.ts +3 -1
- package/dist/analyzer/candidate-signals.js +159 -0
- package/dist/analyzer/candidate-signals.js.map +1 -1
- package/dist/analyzer/llm-learning-gate.d.ts +12 -1
- package/dist/analyzer/llm-learning-gate.js +633 -16
- package/dist/analyzer/llm-learning-gate.js.map +1 -1
- package/dist/cli/commands/claude-hook.js +11 -4
- package/dist/cli/commands/claude-hook.js.map +1 -1
- package/dist/cli/commands/codex.d.ts +60 -0
- package/dist/cli/commands/codex.js +188 -0
- package/dist/cli/commands/codex.js.map +1 -0
- package/dist/cli/commands/doctor.js +35 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/evaluate.d.ts +9 -3
- package/dist/cli/commands/evaluate.js +31 -5
- package/dist/cli/commands/evaluate.js.map +1 -1
- package/dist/cli/commands/init.js +21 -8
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/inspect.js +73 -4
- package/dist/cli/commands/inspect.js.map +1 -1
- package/dist/cli/commands/repair.js +3 -3
- package/dist/cli/commands/repair.js.map +1 -1
- package/dist/cli/commands/status.js +38 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/dispatch.js +16 -4
- package/dist/cli/dispatch.js.map +1 -1
- package/dist/config/config-schema.d.ts +177 -0
- package/dist/config/config-schema.js +142 -1
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/default-config.js +19 -1
- package/dist/config/default-config.js.map +1 -1
- package/dist/config/load-config.js +72 -1
- package/dist/config/load-config.js.map +1 -1
- package/dist/config/settings-store.d.ts +19 -0
- package/dist/config/settings-store.js +11 -0
- package/dist/config/settings-store.js.map +1 -1
- package/dist/controller/candidate-retriever.d.ts +16 -1
- package/dist/controller/candidate-retriever.js +199 -137
- package/dist/controller/candidate-retriever.js.map +1 -1
- package/dist/controller/injection-scorecard.d.ts +2 -14
- package/dist/controller/injection-scorecard.js +29 -0
- package/dist/controller/injection-scorecard.js.map +1 -1
- package/dist/controller/intervention-controller.d.ts +3 -15
- package/dist/controller/intervention-controller.js +219 -57
- package/dist/controller/intervention-controller.js.map +1 -1
- package/dist/controller/policy-enricher.d.ts +10 -0
- package/dist/controller/policy-enricher.js +186 -0
- package/dist/controller/policy-enricher.js.map +1 -0
- package/dist/controller/retrieval-context.d.ts +3 -0
- package/dist/controller/retrieval-context.js +37 -0
- package/dist/controller/retrieval-context.js.map +1 -0
- package/dist/controller/second-opinion-gate.d.ts +41 -0
- package/dist/controller/second-opinion-gate.js +225 -0
- package/dist/controller/second-opinion-gate.js.map +1 -0
- package/dist/controller/trigger-evaluator.d.ts +6 -1
- package/dist/controller/trigger-evaluator.js +31 -1
- package/dist/controller/trigger-evaluator.js.map +1 -1
- package/dist/distillation/prompt-contract.d.ts +1 -1
- package/dist/distillation/prompt-contract.js +3 -1
- package/dist/distillation/prompt-contract.js.map +1 -1
- package/dist/distillation/providers/gemini.js +5 -1
- package/dist/distillation/providers/gemini.js.map +1 -1
- package/dist/distillation/queue-worker.js +22 -3
- package/dist/distillation/queue-worker.js.map +1 -1
- package/dist/evaluation/codex-lifecycle-validation.d.ts +60 -0
- package/dist/evaluation/codex-lifecycle-validation.js +233 -0
- package/dist/evaluation/codex-lifecycle-validation.js.map +1 -0
- package/dist/evaluation/hybrid-phase1-rollout-summary.d.ts +63 -0
- package/dist/evaluation/hybrid-phase1-rollout-summary.js +108 -0
- package/dist/evaluation/hybrid-phase1-rollout-summary.js.map +1 -0
- package/dist/evaluation/hybrid-phase3-gate-metrics.d.ts +26 -0
- package/dist/evaluation/hybrid-phase3-gate-metrics.js +23 -0
- package/dist/evaluation/hybrid-phase3-gate-metrics.js.map +1 -0
- package/dist/evaluation/openclaw-baseline.d.ts +8 -0
- package/dist/evaluation/openclaw-baseline.js +27 -0
- package/dist/evaluation/openclaw-baseline.js.map +1 -1
- package/dist/experience-management/governance-observability.d.ts +13 -0
- package/dist/experience-management/governance-observability.js +37 -0
- package/dist/experience-management/governance-observability.js.map +1 -0
- package/dist/experience-management/node-lifecycle-governance.d.ts +8 -0
- package/dist/experience-management/node-lifecycle-governance.js +80 -0
- package/dist/experience-management/node-lifecycle-governance.js.map +1 -0
- package/dist/experience-management/task-management-signals.d.ts +29 -0
- package/dist/experience-management/task-management-signals.js +148 -0
- package/dist/experience-management/task-management-signals.js.map +1 -0
- package/dist/feedback/feedback-manager.d.ts +4 -1
- package/dist/feedback/feedback-manager.js +11 -22
- package/dist/feedback/feedback-manager.js.map +1 -1
- package/dist/feedback/state-transition.d.ts +6 -1
- package/dist/feedback/state-transition.js +6 -3
- package/dist/feedback/state-transition.js.map +1 -1
- package/dist/hybrid/capsule-builder.d.ts +23 -0
- package/dist/hybrid/capsule-builder.js +114 -0
- package/dist/hybrid/capsule-builder.js.map +1 -0
- package/dist/hybrid/explain-provider-client.d.ts +19 -0
- package/dist/hybrid/explain-provider-client.js +34 -0
- package/dist/hybrid/explain-provider-client.js.map +1 -0
- package/dist/hybrid/postmortem-provider-client.d.ts +19 -0
- package/dist/hybrid/postmortem-provider-client.js +34 -0
- package/dist/hybrid/postmortem-provider-client.js.map +1 -0
- package/dist/hybrid/rollout.d.ts +9 -0
- package/dist/hybrid/rollout.js +49 -0
- package/dist/hybrid/rollout.js.map +1 -0
- package/dist/hybrid/router.d.ts +4 -0
- package/dist/hybrid/router.js +62 -0
- package/dist/hybrid/router.js.map +1 -0
- package/dist/hybrid/types.d.ts +140 -0
- package/dist/hybrid/types.js +2 -0
- package/dist/hybrid/types.js.map +1 -0
- package/dist/hybrid/validators.d.ts +5 -0
- package/dist/hybrid/validators.js +94 -0
- package/dist/hybrid/validators.js.map +1 -0
- package/dist/hybrid/worker-client.d.ts +61 -0
- package/dist/hybrid/worker-client.js +196 -0
- package/dist/hybrid/worker-client.js.map +1 -0
- package/dist/hybrid/workers/explain-decision-llm.d.ts +8 -0
- package/dist/hybrid/workers/explain-decision-llm.js +152 -0
- package/dist/hybrid/workers/explain-decision-llm.js.map +1 -0
- package/dist/hybrid/workers/explain-decision.d.ts +2 -0
- package/dist/hybrid/workers/explain-decision.js +40 -0
- package/dist/hybrid/workers/explain-decision.js.map +1 -0
- package/dist/hybrid/workers/postmortem-review-llm.d.ts +8 -0
- package/dist/hybrid/workers/postmortem-review-llm.js +398 -0
- package/dist/hybrid/workers/postmortem-review-llm.js.map +1 -0
- package/dist/hybrid/workers/postmortem-review.d.ts +2 -0
- package/dist/hybrid/workers/postmortem-review.js +66 -0
- package/dist/hybrid/workers/postmortem-review.js.map +1 -0
- package/dist/install/claude-code-doctor.d.ts +1 -0
- package/dist/install/claude-code-doctor.js +20 -4
- package/dist/install/claude-code-doctor.js.map +1 -1
- package/dist/install/claude-code-installer.js +50 -1
- package/dist/install/claude-code-installer.js.map +1 -1
- package/dist/install/codex-cli.d.ts +15 -0
- package/dist/install/codex-cli.js +55 -3
- package/dist/install/codex-cli.js.map +1 -1
- package/dist/install/codex-installer.d.ts +7 -0
- package/dist/install/codex-installer.js +22 -0
- package/dist/install/codex-installer.js.map +1 -1
- package/dist/install/openclaw-cli.d.ts +11 -0
- package/dist/install/openclaw-cli.js.map +1 -1
- package/dist/install/openclaw-installer.d.ts +12 -7
- package/dist/install/openclaw-installer.js +197 -46
- package/dist/install/openclaw-installer.js.map +1 -1
- package/dist/interaction/service.d.ts +15 -0
- package/dist/interaction/service.js +189 -31
- package/dist/interaction/service.js.map +1 -1
- package/dist/plugin/hooks/before-prompt-build.d.ts +1 -0
- package/dist/plugin/hooks/before-prompt-build.js +4 -1
- package/dist/plugin/hooks/before-prompt-build.js.map +1 -1
- package/dist/plugin/openclaw-install-state.d.ts +39 -0
- package/dist/plugin/openclaw-install-state.js +24 -0
- package/dist/plugin/openclaw-install-state.js.map +1 -0
- package/dist/plugin/openclaw-plugin.d.ts +125 -0
- package/dist/plugin/openclaw-plugin.js +18 -7
- package/dist/plugin/openclaw-plugin.js.map +1 -1
- package/dist/plugin/openclaw-routine-interaction.d.ts +2 -1
- package/dist/plugin/openclaw-routine-interaction.js +12 -7
- package/dist/plugin/openclaw-routine-interaction.js.map +1 -1
- package/dist/plugin/openclaw-runtime-defaults.d.ts +16 -0
- package/dist/plugin/openclaw-runtime-defaults.js +16 -0
- package/dist/plugin/openclaw-runtime-defaults.js.map +1 -0
- package/dist/runtime/service.d.ts +34 -5
- package/dist/runtime/service.js +474 -49
- package/dist/runtime/service.js.map +1 -1
- package/dist/store/sqlite/db.js +28 -0
- package/dist/store/sqlite/db.js.map +1 -1
- package/dist/store/sqlite/repositories/hybrid-invocation-trace-repo.d.ts +11 -0
- package/dist/store/sqlite/repositories/hybrid-invocation-trace-repo.js +76 -0
- package/dist/store/sqlite/repositories/hybrid-invocation-trace-repo.js.map +1 -0
- package/dist/store/sqlite/repositories/hybrid-review-artifact-repo.d.ts +11 -0
- package/dist/store/sqlite/repositories/hybrid-review-artifact-repo.js +73 -0
- package/dist/store/sqlite/repositories/hybrid-review-artifact-repo.js.map +1 -0
- package/dist/store/sqlite/repositories/input-record-repo.d.ts +1 -0
- package/dist/store/sqlite/repositories/input-record-repo.js +13 -0
- package/dist/store/sqlite/repositories/input-record-repo.js.map +1 -1
- package/dist/store/sqlite/repositories/node-repo.d.ts +4 -0
- package/dist/store/sqlite/repositories/node-repo.js +54 -6
- package/dist/store/sqlite/repositories/node-repo.js.map +1 -1
- package/dist/store/sqlite/schema.sql +40 -0
- package/dist/store/vector/embeddings.js +26 -8
- package/dist/store/vector/embeddings.js.map +1 -1
- package/dist/types/domain.d.ts +151 -2
- package/dist/types/plugin.d.ts +2 -1
- package/docs/releases/v0.1.3.md +3 -2
- package/docs/releases/v0.2.0.md +85 -0
- package/docs/releases/v0.2.1.md +21 -0
- package/docs/user-guide.md +44 -13
- package/openclaw.plugin.json +81 -1
- package/package.json +11 -2
- package/plugins/claude-code-experienceengine/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code-experienceengine/scripts/install-deps.sh +1 -1
package/dist/runtime/service.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { LlmLearningGate } from "../analyzer/llm-learning-gate.js";
|
|
2
1
|
import { buildCandidateSignals } from "../analyzer/candidate-signals.js";
|
|
3
2
|
import { buildInjectionScorecard } from "../controller/injection-scorecard.js";
|
|
4
3
|
import { classifyFailureAttributionReason } from "../feedback/automatic-attribution.js";
|
|
@@ -6,13 +5,16 @@ import { applyFeedback } from "../feedback/feedback-manager.js";
|
|
|
6
5
|
import { detectHarm } from "../feedback/harm-detector.js";
|
|
7
6
|
import { createEmptyStats, updateStats } from "../feedback/stats-updater.js";
|
|
8
7
|
import { buildExperienceInput } from "../input/input-adapter.js";
|
|
8
|
+
import { buildRetrievalContext } from "../controller/retrieval-context.js";
|
|
9
9
|
import { resolveScope } from "../input/scope-resolver.js";
|
|
10
10
|
import { decideIntervention } from "../controller/intervention-controller.js";
|
|
11
11
|
import { renderInlineNotice } from "../controller/inline-notice.js";
|
|
12
|
+
import { applyGovernedNodeFeedback, deriveNodeOriginProfileForNode } from "../experience-management/node-lifecycle-governance.js";
|
|
13
|
+
import { resolveHybridRolloutState } from "../hybrid/rollout.js";
|
|
14
|
+
import { selectHybridRoute } from "../hybrid/router.js";
|
|
12
15
|
import { nowIso } from "../utils/clock.js";
|
|
13
16
|
import { createId, stableId } from "../utils/ids.js";
|
|
14
17
|
import { bootstrapDatabase, openDatabase, withTransaction } from "../store/sqlite/db.js";
|
|
15
|
-
import { DistillationQueueWorker } from "../distillation/queue-worker.js";
|
|
16
18
|
import { CandidateRepository } from "../store/sqlite/repositories/candidate-repo.js";
|
|
17
19
|
import { DistillationJobRepository } from "../store/sqlite/repositories/distillation-job-repo.js";
|
|
18
20
|
import { InputRecordRepository } from "../store/sqlite/repositories/input-record-repo.js";
|
|
@@ -23,9 +25,16 @@ import { ScopeRepository } from "../store/sqlite/repositories/scope-repo.js";
|
|
|
23
25
|
import { StatsRepository } from "../store/sqlite/repositories/stats-repo.js";
|
|
24
26
|
import { TaskRunRepository } from "../store/sqlite/repositories/task-run-repo.js";
|
|
25
27
|
import { InjectionRepository } from "../store/sqlite/repositories/injection-repo.js";
|
|
28
|
+
import { HybridInvocationTraceRepository } from "../store/sqlite/repositories/hybrid-invocation-trace-repo.js";
|
|
26
29
|
import { RuntimeCaptureWriter } from "../plugin/runtime-capture.js";
|
|
27
30
|
import { normalizeToolResult } from "../plugin/hooks/tool-result-persist.js";
|
|
28
31
|
import { extractToolResultsFromPayload } from "../plugin/runtime-helpers.js";
|
|
32
|
+
import { HybridReviewArtifactRepository } from "../store/sqlite/repositories/hybrid-review-artifact-repo.js";
|
|
33
|
+
const loadLlmLearningGate = async () => import("../analyzer/llm-learning-gate.js");
|
|
34
|
+
const loadDistillationQueueWorker = async () => import("../distillation/queue-worker.js");
|
|
35
|
+
const loadHybridWorkerClientModule = async () => import("../hybrid/worker-client.js");
|
|
36
|
+
const loadHybridCapsuleBuilder = async () => import("../hybrid/capsule-builder.js");
|
|
37
|
+
const loadHybridPostmortemProviderClient = async () => import("../hybrid/postmortem-provider-client.js");
|
|
29
38
|
const toEvidence = (input) => input.tool_events.map((event) => [event.tool_name, event.status, event.error_signature ?? event.output_summary]
|
|
30
39
|
.filter(Boolean)
|
|
31
40
|
.join(": "));
|
|
@@ -82,9 +91,57 @@ const buildCandidateSourceSignal = (input) => {
|
|
|
82
91
|
failure_signature: signals.failure_signature,
|
|
83
92
|
retry_count: signals.retry_count,
|
|
84
93
|
correction_signals: signals.correction_signals,
|
|
94
|
+
directional_correction: signals.directional_correction,
|
|
95
|
+
evidence_driven_reversal: signals.evidence_driven_reversal,
|
|
85
96
|
tool_event_summary: signals.tool_event_summary
|
|
86
97
|
};
|
|
87
98
|
};
|
|
99
|
+
const mergeDirectionalCorrectionIntoSourceSignal = (sourceSignal, draft) => {
|
|
100
|
+
const directionalCorrection = sourceSignal.directional_correction;
|
|
101
|
+
if (!directionalCorrection) {
|
|
102
|
+
return sourceSignal;
|
|
103
|
+
}
|
|
104
|
+
const semanticDetected = Boolean(draft.experience_kind === "expectation_correction" &&
|
|
105
|
+
draft.correction_category &&
|
|
106
|
+
draft.deviation_pattern &&
|
|
107
|
+
draft.corrected_constraint);
|
|
108
|
+
if (!semanticDetected) {
|
|
109
|
+
return sourceSignal;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...sourceSignal,
|
|
113
|
+
directional_correction: {
|
|
114
|
+
...directionalCorrection,
|
|
115
|
+
semantic_detected: true,
|
|
116
|
+
correction_category: draft.correction_category,
|
|
117
|
+
deviation_pattern: draft.deviation_pattern,
|
|
118
|
+
corrected_constraint: draft.corrected_constraint
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
const mergeEvidenceDrivenReversalIntoSourceSignal = (sourceSignal, draft) => {
|
|
123
|
+
const reversal = sourceSignal.evidence_driven_reversal;
|
|
124
|
+
if (!reversal) {
|
|
125
|
+
return sourceSignal;
|
|
126
|
+
}
|
|
127
|
+
const semanticDetected = Boolean(draft.experience_kind === "expectation_correction" &&
|
|
128
|
+
draft.correction_category &&
|
|
129
|
+
draft.deviation_pattern &&
|
|
130
|
+
draft.corrected_constraint);
|
|
131
|
+
if (!semanticDetected) {
|
|
132
|
+
return sourceSignal;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...sourceSignal,
|
|
136
|
+
evidence_driven_reversal: {
|
|
137
|
+
...reversal,
|
|
138
|
+
semantic_detected: true,
|
|
139
|
+
correction_category: draft.correction_category,
|
|
140
|
+
deviation_pattern: draft.deviation_pattern,
|
|
141
|
+
corrected_constraint: draft.corrected_constraint
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
};
|
|
88
145
|
const summarizeRawCandidate = (sourceSignal) => {
|
|
89
146
|
const fragments = [...sourceSignal.tool_event_summary];
|
|
90
147
|
if (sourceSignal.failure_signature) {
|
|
@@ -104,9 +161,14 @@ const resolveCandidateKind = (input, sourceSignal) => {
|
|
|
104
161
|
}
|
|
105
162
|
return "failure";
|
|
106
163
|
};
|
|
107
|
-
const draftToCandidate = (draft, input, originRecordId, taskRunId) => {
|
|
164
|
+
const draftToCandidate = (draft, input, originRecordId, taskRunId, directionalCorrectionSignal, evidenceDrivenReversalSignal) => {
|
|
108
165
|
const timestamp = nowIso();
|
|
109
|
-
const
|
|
166
|
+
const baseSourceSignal = buildCandidateSourceSignal(input);
|
|
167
|
+
const sourceSignal = mergeEvidenceDrivenReversalIntoSourceSignal(mergeDirectionalCorrectionIntoSourceSignal({
|
|
168
|
+
...baseSourceSignal,
|
|
169
|
+
directional_correction: directionalCorrectionSignal ?? baseSourceSignal.directional_correction,
|
|
170
|
+
evidence_driven_reversal: evidenceDrivenReversalSignal ?? baseSourceSignal.evidence_driven_reversal
|
|
171
|
+
}, draft), draft);
|
|
110
172
|
const candidateId = stableId("candidate", [draft.scope_id, draft.task_type, draft.node_type, draft.compact_hint, originRecordId].join(":"));
|
|
111
173
|
return {
|
|
112
174
|
id: candidateId,
|
|
@@ -138,6 +200,7 @@ const candidateToInitialJob = (candidate, extractorProfile) => {
|
|
|
138
200
|
};
|
|
139
201
|
};
|
|
140
202
|
const mergeContext = (existing, incoming) => ({
|
|
203
|
+
host: incoming.host ?? existing?.host,
|
|
141
204
|
sessionId: incoming.sessionId ?? existing?.sessionId,
|
|
142
205
|
cwd: incoming.cwd ?? existing?.cwd,
|
|
143
206
|
userMessage: incoming.userMessage || existing?.userMessage || "",
|
|
@@ -186,6 +249,23 @@ const resolveDeliveryMode = (evaluationMode, holdoutRate, sessionId, taskSummary
|
|
|
186
249
|
delivered: true
|
|
187
250
|
};
|
|
188
251
|
};
|
|
252
|
+
const HYBRID_LIGHTWEIGHT_PATTERN = /\b(wording-only|wording only|copy-only|copy only|copy pass|inline notice wording|expression-layer refinement)\b/i;
|
|
253
|
+
const isLightweightHybridExcludedTask = (input) => HYBRID_LIGHTWEIGHT_PATTERN.test(`${input.task_summary} ${input.context_summary ?? ""}`);
|
|
254
|
+
export const decidePosttaskHybridRoute = (config, input, signals, rolloutKey = input.task_summary) => {
|
|
255
|
+
const rollout = resolveHybridRolloutState(config, rolloutKey);
|
|
256
|
+
return selectHybridRoute({
|
|
257
|
+
...signals,
|
|
258
|
+
explicitExplanationRequest: false,
|
|
259
|
+
existingConservativePathRequired: false,
|
|
260
|
+
lightweightOrExcludedTask: signals.lightweightOrExcludedTask || isLightweightHybridExcludedTask(input),
|
|
261
|
+
rolloutAllowsAsyncPostmortem: config.hybridAsyncPostmortemEnabled && rollout.hybridActive
|
|
262
|
+
}, {
|
|
263
|
+
enabled: config.hybridEnabled && rollout.hybridActive,
|
|
264
|
+
syncExplainEnabled: false,
|
|
265
|
+
asyncPostmortemEnabled: config.hybridAsyncPostmortemEnabled && rollout.hybridActive,
|
|
266
|
+
policyVersion: config.hybridRoutePolicyVersion
|
|
267
|
+
});
|
|
268
|
+
};
|
|
189
269
|
export class ExperienceRuntimeService {
|
|
190
270
|
config;
|
|
191
271
|
db;
|
|
@@ -202,12 +282,21 @@ export class ExperienceRuntimeService {
|
|
|
202
282
|
reviewEventRepo;
|
|
203
283
|
statsRepo;
|
|
204
284
|
injectionRepo;
|
|
205
|
-
|
|
206
|
-
|
|
285
|
+
hybridReviewArtifactRepo;
|
|
286
|
+
hybridTraceRepo;
|
|
287
|
+
runtimeOptions;
|
|
288
|
+
backgroundLearningEnabled;
|
|
289
|
+
hybridPosttaskEnabled;
|
|
290
|
+
distillationWorkerPromise;
|
|
291
|
+
learningGatePromise;
|
|
292
|
+
hybridWorkerClientPromise;
|
|
207
293
|
pendingLearningTasks = new Set();
|
|
208
294
|
captureWriter;
|
|
209
295
|
constructor(config, logger, runtimeOptions = {}) {
|
|
210
296
|
this.config = config;
|
|
297
|
+
this.runtimeOptions = runtimeOptions;
|
|
298
|
+
this.backgroundLearningEnabled = !runtimeOptions.disableBackgroundLearning;
|
|
299
|
+
this.hybridPosttaskEnabled = !runtimeOptions.disableHybridPosttask;
|
|
211
300
|
this.logger = logger ?? {};
|
|
212
301
|
this.db = openDatabase(config);
|
|
213
302
|
bootstrapDatabase(this.db);
|
|
@@ -221,8 +310,8 @@ export class ExperienceRuntimeService {
|
|
|
221
310
|
this.reviewEventRepo = new ReviewEventRepository(this.db);
|
|
222
311
|
this.statsRepo = new StatsRepository(this.db);
|
|
223
312
|
this.injectionRepo = new InjectionRepository(this.db);
|
|
224
|
-
this.
|
|
225
|
-
this.
|
|
313
|
+
this.hybridReviewArtifactRepo = new HybridReviewArtifactRepository(this.db);
|
|
314
|
+
this.hybridTraceRepo = new HybridInvocationTraceRepository(this.db);
|
|
226
315
|
this.captureWriter = new RuntimeCaptureWriter(config, this.logger);
|
|
227
316
|
}
|
|
228
317
|
getSession(sessionId) {
|
|
@@ -247,8 +336,12 @@ export class ExperienceRuntimeService {
|
|
|
247
336
|
session.toolEventKeys.add(key);
|
|
248
337
|
session.toolEvents.push(toolEvent);
|
|
249
338
|
}
|
|
250
|
-
|
|
251
|
-
|
|
339
|
+
// The shipped runtime path stays exact-scope-only in this rollout.
|
|
340
|
+
resolveExactScopeInjectableNodes(scopeId) {
|
|
341
|
+
return this.nodeRepo.listLiveInjectableByExactScope(scopeId);
|
|
342
|
+
}
|
|
343
|
+
resolveConservativeCrossScopeCandidates(scopeId) {
|
|
344
|
+
return this.nodeRepo.listConservativeCrossScopeCandidates(scopeId);
|
|
252
345
|
}
|
|
253
346
|
recoverToolEvents(sessionId, payload) {
|
|
254
347
|
for (const toolResult of extractToolResultsFromPayload(payload)) {
|
|
@@ -301,8 +394,245 @@ export class ExperienceRuntimeService {
|
|
|
301
394
|
async waitForBackgroundLearning() {
|
|
302
395
|
await Promise.allSettled([...this.pendingLearningTasks]);
|
|
303
396
|
}
|
|
397
|
+
async getLearningGate() {
|
|
398
|
+
if (!this.backgroundLearningEnabled) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
this.learningGatePromise ??= loadLlmLearningGate().then(({ LlmLearningGate: LoadedLlmLearningGate }) => new LoadedLlmLearningGate(this.config, this.runtimeOptions));
|
|
402
|
+
return this.learningGatePromise;
|
|
403
|
+
}
|
|
404
|
+
async getDistillationWorker() {
|
|
405
|
+
if (!this.backgroundLearningEnabled) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
this.distillationWorkerPromise ??= loadDistillationQueueWorker().then(({ DistillationQueueWorker: LoadedDistillationQueueWorker }) => new LoadedDistillationQueueWorker(this.config, this.candidateRepo, this.jobRepo, this.nodeRepo, this.runtimeOptions));
|
|
409
|
+
return this.distillationWorkerPromise;
|
|
410
|
+
}
|
|
411
|
+
async getHybridWorkerClient() {
|
|
412
|
+
if (!this.hybridPosttaskEnabled) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
this.hybridWorkerClientPromise ??= loadHybridWorkerClientModule().then(({ HybridWorkerClient: LoadedHybridWorkerClient }) => new LoadedHybridWorkerClient({
|
|
416
|
+
explainDecisionEnabled: this.config.hybridEnabled && this.config.hybridSyncExplainEnabled,
|
|
417
|
+
postmortemReviewEnabled: this.config.hybridEnabled && this.config.hybridAsyncPostmortemEnabled,
|
|
418
|
+
postmortemReviewLlmEnabled: this.config.hybridEnabled
|
|
419
|
+
&& this.config.hybridAsyncPostmortemEnabled
|
|
420
|
+
&& this.config.hybridAsyncPostmortemLlmEnabled,
|
|
421
|
+
...this.runtimeOptions.hybridWorkerClientOptions
|
|
422
|
+
}));
|
|
423
|
+
return this.hybridWorkerClientPromise;
|
|
424
|
+
}
|
|
425
|
+
buildPostmortemArtifact(input) {
|
|
426
|
+
const timestamp = nowIso();
|
|
427
|
+
return {
|
|
428
|
+
id: createId("hybridreview"),
|
|
429
|
+
task_run_id: input.taskRun.id,
|
|
430
|
+
scope_id: input.taskRun.scope_id,
|
|
431
|
+
worker_task: "postmortem_review",
|
|
432
|
+
approval_class: input.result.approvalClass === "policy_gated" ? "policy_gated" : "review_artifact",
|
|
433
|
+
schema_version: this.config.hybridCapsuleSchemaVersion,
|
|
434
|
+
route_policy_version: input.routeDecision.policyVersion,
|
|
435
|
+
worker_profile_version: this.config.hybridPostmortemReviewProfileVersion,
|
|
436
|
+
recommendation: input.result.value.candidate_recommendation,
|
|
437
|
+
summary: input.result.value.review_artifact?.summary ?? input.result.value.reason,
|
|
438
|
+
payload: input.result.value,
|
|
439
|
+
created_at: timestamp,
|
|
440
|
+
updated_at: timestamp
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
applyPostmortemDeliveryRecommendation(node, recommendation) {
|
|
444
|
+
if (recommendation === "keep" || recommendation === "review") {
|
|
445
|
+
return node;
|
|
446
|
+
}
|
|
447
|
+
if (recommendation === "quarantine") {
|
|
448
|
+
return {
|
|
449
|
+
...node,
|
|
450
|
+
delivery_state: "quarantined",
|
|
451
|
+
quarantined_at: node.quarantined_at ?? nowIso(),
|
|
452
|
+
quarantine_reason: node.quarantine_reason ?? "postmortem_review"
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (node.delivery_state === "quarantined") {
|
|
456
|
+
return node;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
...node,
|
|
460
|
+
delivery_state: node.delivery_state === "shadow_only" ? "shadow_only" : "conservative_only"
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
applyAcceptedPostmortemNodeReviews(input) {
|
|
464
|
+
const reviews = input.result.value.injected_node_reviews ?? [];
|
|
465
|
+
if (!reviews.length || input.taskRun.final_status === "cancelled") {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
const allowedIds = new Set(input.experienceInput.injected_node_ids);
|
|
469
|
+
const existingEvents = this.reviewEventRepo.listByTaskRunId(input.taskRun.id);
|
|
470
|
+
let applied = false;
|
|
471
|
+
for (const review of reviews) {
|
|
472
|
+
if (!allowedIds.has(review.node_id) || review.confidence === "low") {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const current = this.nodeRepo.getById(review.node_id);
|
|
476
|
+
if (!current) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const existingNodeEvents = existingEvents.filter((event) => event.node_id === review.node_id && event.source === "automatic");
|
|
480
|
+
const alreadyMarkedHelped = existingNodeEvents.some((event) => event.event_type === "mark_helped");
|
|
481
|
+
const alreadyMarkedHarmed = existingNodeEvents.some((event) => event.event_type === "mark_harmed");
|
|
482
|
+
let nextNode = current;
|
|
483
|
+
let feedbackEventType;
|
|
484
|
+
if (review.feedback_verdict === "helped" && !alreadyMarkedHelped) {
|
|
485
|
+
nextNode = applyGovernedNodeFeedback(nextNode, "helped", deriveNodeOriginProfileForNode(this.inputRepo, nextNode));
|
|
486
|
+
feedbackEventType = "mark_helped";
|
|
487
|
+
}
|
|
488
|
+
else if (review.feedback_verdict === "harmed" && !alreadyMarkedHarmed) {
|
|
489
|
+
nextNode = applyGovernedNodeFeedback(nextNode, "harmed", deriveNodeOriginProfileForNode(this.inputRepo, nextNode));
|
|
490
|
+
feedbackEventType = "mark_harmed";
|
|
491
|
+
}
|
|
492
|
+
const nodeAfterDelivery = this.applyPostmortemDeliveryRecommendation(nextNode, review.delivery_recommendation);
|
|
493
|
+
if (feedbackEventType
|
|
494
|
+
|| nodeAfterDelivery.delivery_state !== current.delivery_state
|
|
495
|
+
|| nodeAfterDelivery.state !== current.state
|
|
496
|
+
|| nodeAfterDelivery.helped_count !== current.helped_count
|
|
497
|
+
|| nodeAfterDelivery.harmed_count !== current.harmed_count
|
|
498
|
+
|| nodeAfterDelivery.last_feedback_verdict !== current.last_feedback_verdict) {
|
|
499
|
+
this.nodeRepo.upsert(nodeAfterDelivery);
|
|
500
|
+
applied = true;
|
|
501
|
+
}
|
|
502
|
+
if (feedbackEventType) {
|
|
503
|
+
this.reviewEventRepo.upsert({
|
|
504
|
+
id: createId("review"),
|
|
505
|
+
node_id: review.node_id,
|
|
506
|
+
task_run_id: input.taskRun.id,
|
|
507
|
+
event_type: feedbackEventType,
|
|
508
|
+
source: "automatic",
|
|
509
|
+
created_at: nowIso()
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (current.delivery_state !== "quarantined" && nodeAfterDelivery.delivery_state === "quarantined") {
|
|
513
|
+
this.reviewEventRepo.upsert({
|
|
514
|
+
id: createId("review"),
|
|
515
|
+
node_id: review.node_id,
|
|
516
|
+
task_run_id: input.taskRun.id,
|
|
517
|
+
event_type: "quarantine",
|
|
518
|
+
source: "automatic",
|
|
519
|
+
created_at: nowIso()
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return applied;
|
|
524
|
+
}
|
|
525
|
+
async persistHybridPostmortemArtifactAsync(input) {
|
|
526
|
+
if (!this.hybridPosttaskEnabled) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (this.hybridReviewArtifactRepo.getByTaskRunId(input.taskRun.id)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const hybridWorkerClient = await this.getHybridWorkerClient();
|
|
533
|
+
if (!hybridWorkerClient) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const candidateSignals = buildCandidateSignals(input.experienceInput);
|
|
537
|
+
const [{ buildPostmortemReviewCapsule }, { resolveHybridPostmortemProviderEndpoint }] = await Promise.all([
|
|
538
|
+
loadHybridCapsuleBuilder(),
|
|
539
|
+
loadHybridPostmortemProviderClient()
|
|
540
|
+
]);
|
|
541
|
+
const capsule = buildPostmortemReviewCapsule({
|
|
542
|
+
schemaVersion: this.config.hybridCapsuleSchemaVersion,
|
|
543
|
+
routeDecision: input.routeDecision,
|
|
544
|
+
taskRun: input.taskRun,
|
|
545
|
+
outcomeSignal: input.experienceInput.outcome_signal,
|
|
546
|
+
triggers: {
|
|
547
|
+
directionalCorrectionPresent: candidateSignals.directional_correction?.detected === true
|
|
548
|
+
|| candidateSignals.evidence_driven_reversal?.detected === true,
|
|
549
|
+
injectedNodeInteractionPresent: input.experienceInput.injected_node_ids.length > 0,
|
|
550
|
+
retryOrInvalidationSignaturePresent: candidateSignals.retry_count > 0 || candidateSignals.evidence_driven_reversal?.invalidating_evidence === true,
|
|
551
|
+
meaningfulFailureSignaturePresent: Boolean(candidateSignals.failure_signature),
|
|
552
|
+
conservativeTransitionReviewWorthy: input.experienceInput.outcome_signal === "success" && input.experienceInput.injected_node_ids.length > 0
|
|
553
|
+
},
|
|
554
|
+
injectedNodes: input.experienceInput.injected_node_ids
|
|
555
|
+
.map((id) => this.nodeRepo.getById(id))
|
|
556
|
+
.filter((node) => Boolean(node)),
|
|
557
|
+
toolEvents: input.toolEvents
|
|
558
|
+
});
|
|
559
|
+
const providerResolution = this.config.hybridAsyncPostmortemLlmEnabled
|
|
560
|
+
? resolveHybridPostmortemProviderEndpoint(this.config)
|
|
561
|
+
: { status: "disabled", reason: "Phase 3 provider-backed postmortem review is disabled." };
|
|
562
|
+
const result = this.config.hybridAsyncPostmortemLlmEnabled && providerResolution.status === "unavailable"
|
|
563
|
+
? {
|
|
564
|
+
status: "fallback",
|
|
565
|
+
reason: "provider_unavailable"
|
|
566
|
+
}
|
|
567
|
+
: await hybridWorkerClient.runPostmortemReview(capsule, providerResolution.status === "configured"
|
|
568
|
+
? {
|
|
569
|
+
mode: "provider",
|
|
570
|
+
endpoint: providerResolution.endpoint
|
|
571
|
+
}
|
|
572
|
+
: undefined);
|
|
573
|
+
const timestamp = nowIso();
|
|
574
|
+
const persistAcceptedArtifact = result.status === "accepted"
|
|
575
|
+
&& input.rolloutMode !== "shadow"
|
|
576
|
+
&& (result.approvalClass === "review_artifact" || result.approvalClass === "policy_gated");
|
|
577
|
+
const appliedNodeWriteback = result.status === "accepted" && input.rolloutMode !== "shadow"
|
|
578
|
+
? this.applyAcceptedPostmortemNodeReviews({
|
|
579
|
+
taskRun: input.taskRun,
|
|
580
|
+
experienceInput: input.experienceInput,
|
|
581
|
+
result
|
|
582
|
+
})
|
|
583
|
+
: false;
|
|
584
|
+
this.hybridTraceRepo.upsert({
|
|
585
|
+
id: createId("hybridtrace"),
|
|
586
|
+
surface: "runtime",
|
|
587
|
+
session_id: input.taskRun.session_id,
|
|
588
|
+
scope_id: input.taskRun.scope_id,
|
|
589
|
+
worker_task: "postmortem_review",
|
|
590
|
+
route: input.routeDecision.route,
|
|
591
|
+
route_policy_version: input.routeDecision.policyVersion,
|
|
592
|
+
capsule_schema_version: this.config.hybridCapsuleSchemaVersion,
|
|
593
|
+
worker_profile_version: this.config.hybridAsyncPostmortemLlmEnabled
|
|
594
|
+
? this.config.hybridPostmortemModelProfileVersion
|
|
595
|
+
: this.config.hybridPostmortemReviewProfileVersion,
|
|
596
|
+
rollout_mode: input.rolloutMode,
|
|
597
|
+
rollout_reason: input.rolloutReason,
|
|
598
|
+
worker_ran: result.status !== "fallback" || result.reason !== "provider_unavailable",
|
|
599
|
+
validation_status: result.status === "accepted" ? "accepted" : "fallback",
|
|
600
|
+
output_action: persistAcceptedArtifact || appliedNodeWriteback ? "stored" : "rejected",
|
|
601
|
+
fallback_reason: result.status === "accepted" ? undefined : result.reason,
|
|
602
|
+
created_at: timestamp
|
|
603
|
+
});
|
|
604
|
+
if (result.status !== "accepted") {
|
|
605
|
+
this.logger.debug?.("experienceengine.hybrid_postmortem_skipped", {
|
|
606
|
+
taskRunId: input.taskRun.id,
|
|
607
|
+
reason: result.reason
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (persistAcceptedArtifact) {
|
|
612
|
+
this.hybridReviewArtifactRepo.upsert(this.buildPostmortemArtifact({
|
|
613
|
+
taskRun: input.taskRun,
|
|
614
|
+
result,
|
|
615
|
+
routeDecision: input.routeDecision
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
304
619
|
async persistCandidatesAsync(input, originRecordId, taskRunId, sessionId) {
|
|
305
|
-
const
|
|
620
|
+
const learningGate = await this.getLearningGate();
|
|
621
|
+
if (!learningGate) {
|
|
622
|
+
if (taskRunId) {
|
|
623
|
+
const taskRun = this.taskRunRepo.getById(taskRunId);
|
|
624
|
+
if (taskRun) {
|
|
625
|
+
this.taskRunRepo.upsert({
|
|
626
|
+
...taskRun,
|
|
627
|
+
learning_status: "not_applicable",
|
|
628
|
+
learning_reason: "background learning disabled",
|
|
629
|
+
updated_at: nowIso()
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const result = await learningGate.generateCandidateDrafts(input);
|
|
306
636
|
if (taskRunId) {
|
|
307
637
|
const taskRun = this.taskRunRepo.getById(taskRunId);
|
|
308
638
|
if (taskRun) {
|
|
@@ -327,7 +657,7 @@ export class ExperienceRuntimeService {
|
|
|
327
657
|
});
|
|
328
658
|
return;
|
|
329
659
|
}
|
|
330
|
-
const persistedCandidates = result.drafts.map((draft) => draftToCandidate(draft, input, originRecordId, taskRunId));
|
|
660
|
+
const persistedCandidates = result.drafts.map((draft) => draftToCandidate(draft, input, originRecordId, taskRunId, result.directionalCorrectionSignal, result.evidenceDrivenReversalSignal));
|
|
331
661
|
withTransaction(this.db, () => {
|
|
332
662
|
for (const candidate of persistedCandidates) {
|
|
333
663
|
this.candidateRepo.upsert(candidate);
|
|
@@ -342,7 +672,11 @@ export class ExperienceRuntimeService {
|
|
|
342
672
|
reason: result.reason
|
|
343
673
|
});
|
|
344
674
|
if (this.config.distillationAutoDrain) {
|
|
345
|
-
await this.
|
|
675
|
+
const distillationWorker = await this.getDistillationWorker();
|
|
676
|
+
if (!distillationWorker) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
await distillationWorker.drain().catch((error) => {
|
|
346
680
|
this.logger.error?.("experienceengine.distillation_drain_failed", {
|
|
347
681
|
sessionId,
|
|
348
682
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -350,7 +684,7 @@ export class ExperienceRuntimeService {
|
|
|
350
684
|
});
|
|
351
685
|
}
|
|
352
686
|
}
|
|
353
|
-
updateInjectedNodes(input, attributionRecordId, taskRunId) {
|
|
687
|
+
updateInjectedNodes(input, attributionRecordId, taskRunId, injectionEvent) {
|
|
354
688
|
if (!input.injected_node_ids.length) {
|
|
355
689
|
return;
|
|
356
690
|
}
|
|
@@ -362,7 +696,7 @@ export class ExperienceRuntimeService {
|
|
|
362
696
|
if (input.outcome_signal === "success") {
|
|
363
697
|
return {
|
|
364
698
|
nodeId: node.id,
|
|
365
|
-
eventType: "
|
|
699
|
+
eventType: "mark_uncertain"
|
|
366
700
|
};
|
|
367
701
|
}
|
|
368
702
|
if (detectHarm(input, node)) {
|
|
@@ -374,8 +708,35 @@ export class ExperienceRuntimeService {
|
|
|
374
708
|
return undefined;
|
|
375
709
|
})
|
|
376
710
|
.filter((value) => Boolean(value));
|
|
377
|
-
|
|
378
|
-
this.
|
|
711
|
+
const originProfilesByNodeId = Object.fromEntries(touched.map((node) => {
|
|
712
|
+
return [node.id, deriveNodeOriginProfileForNode(this.inputRepo, node)];
|
|
713
|
+
}));
|
|
714
|
+
const highMatchPromotionIds = new Set(injectionEvent?.scorecard?.topCandidates
|
|
715
|
+
?.filter((candidate) => candidate.matchScorecard?.scopeMatch === "same" &&
|
|
716
|
+
candidate.matchScorecard.overallMatchBand === "high" &&
|
|
717
|
+
candidate.matchScorecard.negativeEvidence.length === 0)
|
|
718
|
+
.map((candidate) => candidate.id) ?? []);
|
|
719
|
+
const promotedNodeIds = [];
|
|
720
|
+
for (const node of applyFeedback(input, touched, attributionRecordId, { originProfilesByNodeId })) {
|
|
721
|
+
const shouldPromoteSameScopeHighMatch = input.outcome_signal === "success" &&
|
|
722
|
+
input.scope_id === node.scope_id &&
|
|
723
|
+
highMatchPromotionIds.has(node.id) &&
|
|
724
|
+
node.state === "priority_candidate" &&
|
|
725
|
+
node.delivery_state === "conservative_only" &&
|
|
726
|
+
node.harmed_count === 0;
|
|
727
|
+
const nextNode = shouldPromoteSameScopeHighMatch
|
|
728
|
+
? {
|
|
729
|
+
...node,
|
|
730
|
+
state: "active",
|
|
731
|
+
delivery_state: "eligible",
|
|
732
|
+
validation_state: node.validation_state ?? "validated_by_reuse",
|
|
733
|
+
promotion_reason: node.promotion_reason ?? "same_scope_high_match_success"
|
|
734
|
+
}
|
|
735
|
+
: node;
|
|
736
|
+
if (shouldPromoteSameScopeHighMatch) {
|
|
737
|
+
promotedNodeIds.push(node.id);
|
|
738
|
+
}
|
|
739
|
+
this.nodeRepo.upsert(nextNode);
|
|
379
740
|
}
|
|
380
741
|
for (const event of automaticEvents) {
|
|
381
742
|
this.reviewEventRepo.upsert({
|
|
@@ -387,12 +748,23 @@ export class ExperienceRuntimeService {
|
|
|
387
748
|
created_at: nowIso()
|
|
388
749
|
});
|
|
389
750
|
}
|
|
751
|
+
for (const nodeId of promotedNodeIds) {
|
|
752
|
+
this.reviewEventRepo.upsert({
|
|
753
|
+
id: createId("review"),
|
|
754
|
+
node_id: nodeId,
|
|
755
|
+
task_run_id: taskRunId,
|
|
756
|
+
event_type: "promote_eligible",
|
|
757
|
+
source: "automatic",
|
|
758
|
+
created_at: nowIso()
|
|
759
|
+
});
|
|
760
|
+
}
|
|
390
761
|
}
|
|
391
762
|
async beforePromptBuild(context) {
|
|
392
763
|
const sessionId = context.sessionId ?? "global";
|
|
393
764
|
const session = this.getSession(sessionId);
|
|
394
765
|
session.context = mergeContext(session.context, context);
|
|
395
766
|
const input = buildExperienceInput(session.context, session.toolEvents);
|
|
767
|
+
const retrievalContext = buildRetrievalContext(input, session.context);
|
|
396
768
|
const resolvedScope = resolveScope(session.context.cwd);
|
|
397
769
|
const existingScope = this.scopeRepo.getById(resolvedScope.scope_id);
|
|
398
770
|
if (existingScope?.is_disabled) {
|
|
@@ -410,6 +782,7 @@ export class ExperienceRuntimeService {
|
|
|
410
782
|
mode: "skip",
|
|
411
783
|
text: undefined,
|
|
412
784
|
notice: undefined,
|
|
785
|
+
retrievalContext,
|
|
413
786
|
input: {
|
|
414
787
|
...input,
|
|
415
788
|
scope_id: existingScope.scope_id,
|
|
@@ -418,8 +791,13 @@ export class ExperienceRuntimeService {
|
|
|
418
791
|
};
|
|
419
792
|
}
|
|
420
793
|
const stats = input.task_type !== "unknown" ? this.statsRepo.get(input.scope_id, input.task_type) : undefined;
|
|
421
|
-
const nodes = input.task_type !== "unknown"
|
|
422
|
-
|
|
794
|
+
const nodes = input.task_type !== "unknown"
|
|
795
|
+
? [
|
|
796
|
+
...this.resolveExactScopeInjectableNodes(input.scope_id),
|
|
797
|
+
...this.resolveConservativeCrossScopeCandidates(input.scope_id)
|
|
798
|
+
]
|
|
799
|
+
: [];
|
|
800
|
+
const decision = await decideIntervention(input, nodes, stats, this.config.triggerThreshold, this.config.maxHints, this.config, retrievalContext);
|
|
423
801
|
const selectedNodeIds = decision.selected.map((node) => node.id);
|
|
424
802
|
const delivery = resolveDeliveryMode(this.config.evaluationMode, this.config.holdoutRate, sessionId, input.task_summary, decision.mode !== "skip" && selectedNodeIds.length > 0);
|
|
425
803
|
session.injectedNodeIds = delivery.delivered ? selectedNodeIds : [];
|
|
@@ -427,30 +805,27 @@ export class ExperienceRuntimeService {
|
|
|
427
805
|
...session.context,
|
|
428
806
|
injectedNodeIds: session.injectedNodeIds
|
|
429
807
|
};
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
else {
|
|
452
|
-
session.lastInjectionEvent = undefined;
|
|
453
|
-
}
|
|
808
|
+
const scorecard = decision.mode !== "skip"
|
|
809
|
+
? buildInjectionScorecard(input, decision.mode, decision.selected, sessionId, decision.diagnostics)
|
|
810
|
+
: undefined;
|
|
811
|
+
const injectionEvent = {
|
|
812
|
+
injection_id: createId(decision.mode === "skip" ? "decision" : "inject"),
|
|
813
|
+
session_id: sessionId,
|
|
814
|
+
scope_id: input.scope_id,
|
|
815
|
+
task_type: input.task_type === "unknown" ? "general" : input.task_type,
|
|
816
|
+
task_summary: input.task_summary,
|
|
817
|
+
mode: decision.mode,
|
|
818
|
+
delivery_mode: delivery.deliveryMode,
|
|
819
|
+
delivered: delivery.delivered,
|
|
820
|
+
injected_node_ids: selectedNodeIds,
|
|
821
|
+
injection_count: selectedNodeIds.length,
|
|
822
|
+
scorecard,
|
|
823
|
+
was_successful: null,
|
|
824
|
+
harm_observed: null,
|
|
825
|
+
created_at: nowIso()
|
|
826
|
+
};
|
|
827
|
+
this.injectionRepo.upsert(injectionEvent);
|
|
828
|
+
session.lastInjectionEvent = injectionEvent;
|
|
454
829
|
this.logger.debug?.("experienceengine.before_prompt_build", {
|
|
455
830
|
sessionId,
|
|
456
831
|
mode: decision.mode,
|
|
@@ -466,6 +841,7 @@ export class ExperienceRuntimeService {
|
|
|
466
841
|
scorecard: decision.mode !== "skip" ? session.lastInjectionEvent?.scorecard : undefined,
|
|
467
842
|
deliveryMode: decision.mode !== "skip" ? delivery.deliveryMode : undefined,
|
|
468
843
|
delivered: decision.mode !== "skip" ? delivery.delivered : undefined,
|
|
844
|
+
retrievalContext,
|
|
469
845
|
input: {
|
|
470
846
|
...input,
|
|
471
847
|
injected_node_ids: session.injectedNodeIds
|
|
@@ -499,7 +875,7 @@ export class ExperienceRuntimeService {
|
|
|
499
875
|
const taskRun = toTaskRun(input, sessionId, context);
|
|
500
876
|
this.taskRunRepo.upsert(taskRun);
|
|
501
877
|
this.outcomeRepo.upsert(toOutcomeRecord(taskRun, input));
|
|
502
|
-
this.updateInjectedNodes(input, record.record_id, taskRun.id);
|
|
878
|
+
this.updateInjectedNodes(input, record.record_id, taskRun.id, session.lastInjectionEvent);
|
|
503
879
|
if (session.lastInjectionEvent) {
|
|
504
880
|
const touchedNodes = session.lastInjectionEvent.injected_node_ids
|
|
505
881
|
.map((id) => this.nodeRepo.getById(id))
|
|
@@ -527,22 +903,71 @@ export class ExperienceRuntimeService {
|
|
|
527
903
|
input,
|
|
528
904
|
originRecordId: record.record_id,
|
|
529
905
|
taskRunId: taskRun.id,
|
|
530
|
-
sessionId
|
|
906
|
+
sessionId,
|
|
907
|
+
taskRun,
|
|
908
|
+
toolEvents: [...session.toolEvents]
|
|
531
909
|
};
|
|
532
910
|
});
|
|
533
911
|
this.sessions.delete(sessionId);
|
|
534
|
-
|
|
535
|
-
|
|
912
|
+
const rollout = resolveHybridRolloutState(this.config, `${sessionId}:${input.task_summary}`);
|
|
913
|
+
const hybridPosttaskRoute = decidePosttaskHybridRoute(this.config, input, {
|
|
914
|
+
taskStage: "posttask",
|
|
915
|
+
completedRun: true,
|
|
916
|
+
terminalOutcomeRecorded: true,
|
|
917
|
+
boundedPosttaskCapsuleAvailable: Boolean(input.task_summary),
|
|
918
|
+
postmortemAlreadyRecorded: learningTaskContext
|
|
919
|
+
? Boolean(this.hybridReviewArtifactRepo.getByTaskRunId(learningTaskContext.taskRun.id))
|
|
920
|
+
: false,
|
|
921
|
+
lightweightOrExcludedTask: false,
|
|
922
|
+
directionalCorrectionPresent: Boolean(learningTaskContext
|
|
923
|
+
? buildCandidateSignals(learningTaskContext.input).directional_correction?.detected
|
|
924
|
+
|| buildCandidateSignals(learningTaskContext.input).evidence_driven_reversal?.detected
|
|
925
|
+
: false),
|
|
926
|
+
injectedNodeInteractionPresent: input.injected_node_ids.length > 0,
|
|
927
|
+
retryOrInvalidationSignaturePresent: Boolean(learningTaskContext
|
|
928
|
+
? buildCandidateSignals(learningTaskContext.input).retry_count > 0
|
|
929
|
+
|| buildCandidateSignals(learningTaskContext.input).evidence_driven_reversal?.invalidating_evidence
|
|
930
|
+
: false),
|
|
931
|
+
meaningfulFailureSignaturePresent: Boolean(learningTaskContext
|
|
932
|
+
? buildCandidateSignals(learningTaskContext.input).failure_signature
|
|
933
|
+
: input.outcome_signal === "failure"),
|
|
934
|
+
conservativeTransitionReviewWorthy: false
|
|
935
|
+
}, `${sessionId}:${input.task_summary}`);
|
|
936
|
+
if (learningTaskContext && this.backgroundLearningEnabled) {
|
|
937
|
+
this.trackLearningTask((async () => {
|
|
938
|
+
await this.persistCandidatesAsync(learningTaskContext.input, learningTaskContext.originRecordId, learningTaskContext.taskRunId, learningTaskContext.sessionId);
|
|
939
|
+
if (hybridPosttaskRoute.route !== "ESCALATE_ASYNC_POSTMORTEM") {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const refreshedTaskRun = this.taskRunRepo.getById(learningTaskContext.taskRun.id) ?? learningTaskContext.taskRun;
|
|
943
|
+
await this.persistHybridPostmortemArtifactAsync({
|
|
944
|
+
taskRun: refreshedTaskRun,
|
|
945
|
+
experienceInput: learningTaskContext.input,
|
|
946
|
+
routeDecision: hybridPosttaskRoute,
|
|
947
|
+
toolEvents: learningTaskContext.toolEvents,
|
|
948
|
+
rolloutMode: rollout.effectiveMode,
|
|
949
|
+
rolloutReason: rollout.reason
|
|
950
|
+
});
|
|
951
|
+
})());
|
|
536
952
|
}
|
|
537
953
|
this.logger.info?.("experienceengine.finalize", {
|
|
538
954
|
sessionId,
|
|
539
955
|
taskType: input.task_type,
|
|
540
|
-
outcome: input.outcome_signal
|
|
956
|
+
outcome: input.outcome_signal,
|
|
957
|
+
hybridPosttaskRoute: hybridPosttaskRoute.route,
|
|
958
|
+
hybridPosttaskRouteReason: hybridPosttaskRoute.reasonCode,
|
|
959
|
+
hybridRoutePolicyVersion: hybridPosttaskRoute.policyVersion,
|
|
960
|
+
hybridRolloutMode: rollout.effectiveMode,
|
|
961
|
+
hybridRolloutReason: rollout.reason
|
|
541
962
|
});
|
|
542
963
|
return input;
|
|
543
964
|
}
|
|
544
965
|
async drainDistillationQueue(limit) {
|
|
545
|
-
|
|
966
|
+
const distillationWorker = await this.getDistillationWorker();
|
|
967
|
+
if (!distillationWorker) {
|
|
968
|
+
return 0;
|
|
969
|
+
}
|
|
970
|
+
return distillationWorker.drain(limit);
|
|
546
971
|
}
|
|
547
972
|
}
|
|
548
973
|
//# sourceMappingURL=service.js.map
|