rlhf-feedback-loop 0.6.11 → 0.6.13
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 +10 -0
- package/README.md +116 -74
- package/adapters/README.md +3 -3
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +2 -0
- package/adapters/chatgpt/INSTALL.md +7 -4
- package/adapters/chatgpt/openapi.yaml +6 -3
- package/adapters/claude/.mcp.json +3 -3
- package/adapters/codex/config.toml +3 -3
- package/adapters/gemini/function-declarations.json +2 -2
- package/adapters/mcp/server-stdio.js +19 -5
- package/bin/cli.js +302 -32
- package/openapi/openapi.yaml +6 -3
- package/package.json +22 -9
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/adk-consolidator.js +126 -32
- package/scripts/billing.js +192 -685
- package/scripts/context-engine.js +81 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/feedback-ingest-watcher.js +290 -0
- package/scripts/feedback-loop.js +154 -9
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-schema.js +31 -5
- package/scripts/feedback-to-memory.js +13 -1
- package/scripts/generate-paperbanana-diagrams.sh +1 -1
- package/scripts/hook-auto-capture.sh +6 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/install-mcp.js +168 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/local-model-profile.js +207 -0
- package/scripts/pr-manager.js +112 -0
- package/scripts/prove-adapters.js +137 -15
- package/scripts/prove-automation.js +41 -8
- package/scripts/prove-lancedb.js +1 -1
- package/scripts/prove-local-intelligence.js +244 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +7 -1
- package/scripts/status-dashboard.js +155 -0
- package/scripts/test-coverage.js +1 -1
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +115 -17
- package/src/api/server.js +372 -25
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.join(__dirname, '..');
|
|
10
|
+
const DEFAULT_PROOF_DIR = process.env.RLHF_PROOF_DIR || path.join(ROOT, 'proof');
|
|
11
|
+
|
|
12
|
+
function ensureDir(dirPath) {
|
|
13
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runTests() {
|
|
17
|
+
try {
|
|
18
|
+
return execSync(
|
|
19
|
+
'node --test tests/local-model-profile.test.js tests/risk-scorer.test.js tests/vector-store.test.js tests/feedback-sequences.test.js tests/feedback-loop.test.js',
|
|
20
|
+
{ cwd: ROOT, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
21
|
+
);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return err.stdout || err.stderr || String(err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseTestOutput(output) {
|
|
28
|
+
const passMatch = output.match(/ℹ pass (\d+)/);
|
|
29
|
+
const failMatch = output.match(/ℹ fail (\d+)/);
|
|
30
|
+
return {
|
|
31
|
+
passed: passMatch ? Number(passMatch[1]) : 0,
|
|
32
|
+
failed: failMatch ? Number(failMatch[1]) : 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const output = runTests();
|
|
38
|
+
const testResults = parseTestOutput(output);
|
|
39
|
+
const proofDir = DEFAULT_PROOF_DIR;
|
|
40
|
+
ensureDir(proofDir);
|
|
41
|
+
|
|
42
|
+
const tmpFeedbackDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rlhf-local-intel-'));
|
|
43
|
+
const report = {
|
|
44
|
+
generatedAt: new Date().toISOString(),
|
|
45
|
+
checks: [],
|
|
46
|
+
summary: { passed: 0, failed: 0 },
|
|
47
|
+
testResults,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function addResult(id, passed, evidence) {
|
|
51
|
+
report.checks.push({ id, passed, evidence });
|
|
52
|
+
if (passed) report.summary.passed += 1;
|
|
53
|
+
else report.summary.failed += 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const { writeModelFitReport } = require('./local-model-profile');
|
|
58
|
+
const { reportPath, report: modelFitReport } = writeModelFitReport(tmpFeedbackDir, {
|
|
59
|
+
resolved: require('./local-model-profile').resolveEmbeddingProfile({
|
|
60
|
+
RLHF_RAM_BYTES_OVERRIDE: String(4 * 1024 ** 3),
|
|
61
|
+
RLHF_CPU_COUNT_OVERRIDE: '4',
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
addResult(
|
|
65
|
+
'FIT-01',
|
|
66
|
+
fs.existsSync(reportPath) && modelFitReport.selectedProfile.id === 'compact',
|
|
67
|
+
`model-fit report written; selected profile=${modelFitReport.selectedProfile.id}; maxChars=${modelFitReport.selectedProfile.maxChars}`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
process.env.RLHF_FEEDBACK_DIR = tmpFeedbackDir;
|
|
71
|
+
process.env.RLHF_MODEL_FIT_PROFILE = 'quality';
|
|
72
|
+
process.env.RLHF_VECTOR_FORCE_PRIMARY_FAILURE = 'true';
|
|
73
|
+
delete process.env.RLHF_VECTOR_STUB_EMBED;
|
|
74
|
+
delete require.cache[require.resolve('./vector-store')];
|
|
75
|
+
const vectorStore = require('./vector-store');
|
|
76
|
+
vectorStore.setLanceLoaderForTests(async () => {
|
|
77
|
+
const tables = new Map();
|
|
78
|
+
return {
|
|
79
|
+
connect: async () => ({
|
|
80
|
+
tableNames: async () => [...tables.keys()],
|
|
81
|
+
openTable: async (name) => {
|
|
82
|
+
const rows = tables.get(name) || [];
|
|
83
|
+
return {
|
|
84
|
+
add: async (records) => {
|
|
85
|
+
rows.push(...records);
|
|
86
|
+
tables.set(name, rows);
|
|
87
|
+
},
|
|
88
|
+
search: () => ({
|
|
89
|
+
limit: (limit) => ({
|
|
90
|
+
toArray: async () => rows.slice(0, limit),
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
createTable: async (name, records) => {
|
|
96
|
+
tables.set(name, [...records]);
|
|
97
|
+
return {
|
|
98
|
+
add: async (more) => {
|
|
99
|
+
const rows = tables.get(name) || [];
|
|
100
|
+
rows.push(...more);
|
|
101
|
+
tables.set(name, rows);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
vectorStore.setPipelineLoaderForTests(async (_task, model, opts) => async () => ({
|
|
109
|
+
data: Float32Array.from({ length: 384 }, (_, index) => (index === 0 ? 1 : 0)),
|
|
110
|
+
model,
|
|
111
|
+
opts,
|
|
112
|
+
}));
|
|
113
|
+
await vectorStore.upsertFeedback({
|
|
114
|
+
id: 'proof-local-intel',
|
|
115
|
+
signal: 'positive',
|
|
116
|
+
context: 'vector fallback proof',
|
|
117
|
+
tags: ['proof'],
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
const fallbackProfile = vectorStore.getLastEmbeddingProfile();
|
|
121
|
+
addResult(
|
|
122
|
+
'FIT-02',
|
|
123
|
+
Boolean(fallbackProfile && fallbackProfile.fallbackUsed),
|
|
124
|
+
`vector-store active profile=${fallbackProfile && fallbackProfile.activeProfile ? fallbackProfile.activeProfile.id : 'none'}; fallbackUsed=${fallbackProfile ? fallbackProfile.fallbackUsed : false}; reason=${fallbackProfile ? fallbackProfile.fallbackReason : 'n/a'}`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
delete require.cache[require.resolve('./feedback-loop')];
|
|
128
|
+
const { captureFeedback, analyzeFeedback } = require('./feedback-loop');
|
|
129
|
+
captureFeedback({
|
|
130
|
+
signal: 'up',
|
|
131
|
+
context: 'ran tests and included logs',
|
|
132
|
+
whatWorked: 'verification complete',
|
|
133
|
+
tags: ['testing', 'verification'],
|
|
134
|
+
});
|
|
135
|
+
captureFeedback({
|
|
136
|
+
signal: 'down',
|
|
137
|
+
context: 'skipped tests and missing logs caused failure',
|
|
138
|
+
whatWentWrong: 'verification skipped',
|
|
139
|
+
whatToChange: 'always run tests',
|
|
140
|
+
tags: ['debugging', 'verification'],
|
|
141
|
+
});
|
|
142
|
+
captureFeedback({
|
|
143
|
+
signal: 'up',
|
|
144
|
+
context: 'proof attached and verification complete',
|
|
145
|
+
whatWorked: 'full evidence',
|
|
146
|
+
tags: ['testing', 'verification'],
|
|
147
|
+
});
|
|
148
|
+
captureFeedback({
|
|
149
|
+
signal: 'down',
|
|
150
|
+
context: 'unsafe path and security risk caused rejection',
|
|
151
|
+
whatWentWrong: 'unsafe path',
|
|
152
|
+
whatToChange: 'validate paths',
|
|
153
|
+
tags: ['security'],
|
|
154
|
+
});
|
|
155
|
+
const clarification = captureFeedback({
|
|
156
|
+
signal: 'up',
|
|
157
|
+
context: 'thumbs up',
|
|
158
|
+
tags: ['verification'],
|
|
159
|
+
});
|
|
160
|
+
addResult(
|
|
161
|
+
'VETO-01',
|
|
162
|
+
clarification.status === 'clarification_required' && clarification.needsClarification === true,
|
|
163
|
+
`vague feedback status=${clarification.status}; prompt=${clarification.prompt || 'n/a'}`
|
|
164
|
+
);
|
|
165
|
+
captureFeedback({
|
|
166
|
+
signal: 'positive',
|
|
167
|
+
context: 'claimed success without logs',
|
|
168
|
+
whatWorked: 'Reviewer approved despite missing logs',
|
|
169
|
+
tags: ['verification'],
|
|
170
|
+
rubricScores: [
|
|
171
|
+
{ criterion: 'verification_evidence', score: 5, judge: 'judge-a' },
|
|
172
|
+
{ criterion: 'verification_evidence', score: 2, judge: 'judge-b', evidence: 'missing logs' },
|
|
173
|
+
],
|
|
174
|
+
guardrails: {
|
|
175
|
+
testsPassed: false,
|
|
176
|
+
pathSafety: true,
|
|
177
|
+
budgetCompliant: true,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
captureFeedback({
|
|
181
|
+
signal: 'down',
|
|
182
|
+
context: 'regression due to skipped verification',
|
|
183
|
+
whatWentWrong: 'regression shipped',
|
|
184
|
+
whatToChange: 'add regression tests',
|
|
185
|
+
tags: ['debugging', 'verification'],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const riskModelPath = path.join(tmpFeedbackDir, 'risk-model.json');
|
|
189
|
+
const analysis = analyzeFeedback();
|
|
190
|
+
addResult(
|
|
191
|
+
'RISK-01',
|
|
192
|
+
fs.existsSync(riskModelPath),
|
|
193
|
+
'risk-model artifact written'
|
|
194
|
+
);
|
|
195
|
+
addResult(
|
|
196
|
+
'RISK-02',
|
|
197
|
+
Boolean(analysis.boostedRisk && analysis.boostedRisk.exampleCount >= 6),
|
|
198
|
+
`boostedRisk exampleCount=${analysis.boostedRisk ? analysis.boostedRisk.exampleCount : 0}; mode=${analysis.boostedRisk ? analysis.boostedRisk.mode : 'none'}; topDomain=${analysis.boostedRisk && analysis.boostedRisk.highRiskDomains[0] ? analysis.boostedRisk.highRiskDomains[0].key : 'none'}`
|
|
199
|
+
);
|
|
200
|
+
} finally {
|
|
201
|
+
delete process.env.RLHF_FEEDBACK_DIR;
|
|
202
|
+
delete process.env.RLHF_MODEL_FIT_PROFILE;
|
|
203
|
+
delete process.env.RLHF_VECTOR_FORCE_PRIMARY_FAILURE;
|
|
204
|
+
delete process.env.RLHF_VECTOR_STUB_EMBED;
|
|
205
|
+
fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const passed = report.summary.failed === 0 && report.testResults.failed === 0;
|
|
209
|
+
const jsonPath = path.join(proofDir, 'local-intelligence-report.json');
|
|
210
|
+
const mdPath = path.join(proofDir, 'local-intelligence-report.md');
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
213
|
+
|
|
214
|
+
const lines = [
|
|
215
|
+
'# Local Intelligence Proof Report',
|
|
216
|
+
'',
|
|
217
|
+
`Status: ${passed ? 'PASSED' : 'FAILED'}`,
|
|
218
|
+
`Generated: ${report.generatedAt}`,
|
|
219
|
+
'',
|
|
220
|
+
'## Test Results',
|
|
221
|
+
'',
|
|
222
|
+
`- Passed: ${report.testResults.passed}`,
|
|
223
|
+
`- Failed: ${report.testResults.failed}`,
|
|
224
|
+
'',
|
|
225
|
+
'## Checks',
|
|
226
|
+
'',
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
report.checks.forEach((check) => {
|
|
230
|
+
lines.push(`- ${check.id}: ${check.passed ? 'PASS' : 'FAIL'} — ${check.evidence}`);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
fs.writeFileSync(mdPath, `${lines.join('\n')}\n`);
|
|
234
|
+
|
|
235
|
+
process.stdout.write(`Status: ${passed ? 'PASSED' : 'FAILED'}\n`);
|
|
236
|
+
process.stdout.write(`JSON report: ${jsonPath}\n`);
|
|
237
|
+
process.stdout.write(`Markdown report: ${mdPath}\n`);
|
|
238
|
+
process.exit(passed ? 0 : 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main().catch((err) => {
|
|
242
|
+
console.error(`prove-local-intelligence failed: ${err.message}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const {
|
|
5
|
+
runWorkflowContractValidation,
|
|
6
|
+
} = require('./validate-workflow-contract');
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
9
|
+
const DEFAULT_PROOF_DIR = path.join(PROJECT_ROOT, 'proof', 'workflow-contract');
|
|
10
|
+
|
|
11
|
+
function ensureDir(dirPath) {
|
|
12
|
+
if (!fs.existsSync(dirPath)) {
|
|
13
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toMarkdown(report) {
|
|
18
|
+
const lines = [
|
|
19
|
+
'# Workflow Contract Proof Report',
|
|
20
|
+
'',
|
|
21
|
+
`Generated: ${report.generatedAt}`,
|
|
22
|
+
'',
|
|
23
|
+
`Summary: ${report.summary.passed} passed, ${report.summary.failed} failed`,
|
|
24
|
+
'',
|
|
25
|
+
'## Validated Files',
|
|
26
|
+
'',
|
|
27
|
+
...Object.values(report.files).map((filePath) => `- \`${filePath}\``),
|
|
28
|
+
'',
|
|
29
|
+
'## Checks',
|
|
30
|
+
'',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
report.checks.forEach((check) => {
|
|
34
|
+
lines.push(`- ${check.name}: ${check.passed ? 'PASS' : 'FAIL'}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (report.issues.length > 0) {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push('## Issues');
|
|
40
|
+
lines.push('');
|
|
41
|
+
report.issues.forEach((issue) => {
|
|
42
|
+
lines.push(`- ${issue}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${lines.join('\n')}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runWorkflowContractProof(options = {}) {
|
|
50
|
+
const proofDir = options.proofDir || process.env.RLHF_WORKFLOW_CONTRACT_PROOF_DIR || DEFAULT_PROOF_DIR;
|
|
51
|
+
const writeArtifacts = options.writeArtifacts !== false;
|
|
52
|
+
const validation = runWorkflowContractValidation({ projectRoot: options.projectRoot || PROJECT_ROOT });
|
|
53
|
+
|
|
54
|
+
const report = {
|
|
55
|
+
generatedAt: validation.generatedAt,
|
|
56
|
+
files: validation.files,
|
|
57
|
+
checks: [
|
|
58
|
+
{
|
|
59
|
+
name: 'workflow.contract.complete',
|
|
60
|
+
passed: validation.ok,
|
|
61
|
+
details: {
|
|
62
|
+
headingsFound: validation.details.workflow ? validation.details.workflow.headingsFound : [],
|
|
63
|
+
proofCommandsFound: validation.details.workflow ? validation.details.workflow.proofCommandsFound : [],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'issue.template.complete',
|
|
68
|
+
passed: validation.ok,
|
|
69
|
+
details: {
|
|
70
|
+
fieldIdsFound: validation.details.issueTemplate ? validation.details.issueTemplate.fieldIdsFound : [],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'pull_request.template.complete',
|
|
75
|
+
passed: validation.ok,
|
|
76
|
+
details: {
|
|
77
|
+
sectionsFound: validation.details.pullRequestTemplate ? validation.details.pullRequestTemplate.sectionsFound : [],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'readme.links.contracts',
|
|
82
|
+
passed: validation.ok,
|
|
83
|
+
details: validation.details.readme || {},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
issues: validation.issues.slice(),
|
|
87
|
+
summary: {
|
|
88
|
+
passed: validation.ok ? 4 : 0,
|
|
89
|
+
failed: validation.ok ? 0 : 4,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (writeArtifacts) {
|
|
94
|
+
ensureDir(proofDir);
|
|
95
|
+
fs.writeFileSync(path.join(proofDir, 'report.json'), JSON.stringify(report, null, 2));
|
|
96
|
+
fs.writeFileSync(path.join(proofDir, 'report.md'), toMarkdown(report));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return report;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (require.main === module) {
|
|
103
|
+
const report = runWorkflowContractProof();
|
|
104
|
+
if (report.summary.failed > 0) {
|
|
105
|
+
console.error(toMarkdown(report));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(toMarkdown(report));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
DEFAULT_PROOF_DIR,
|
|
114
|
+
runWorkflowContractProof,
|
|
115
|
+
toMarkdown,
|
|
116
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
7
|
+
const DEFAULT_STATE_PATH = path.join(PROJECT_ROOT, '.rlhf', 'reminder-state.json');
|
|
8
|
+
|
|
9
|
+
const REMINDER_TEMPLATES = {
|
|
10
|
+
guardrail_spike: 'Safety guardrails triggered {{count}} times. Re-apply rule: {{rule}}',
|
|
11
|
+
iteration_limit: 'Approaching max iterations ({{count}}/{{limit}}). Prioritize essential actions only.',
|
|
12
|
+
tool_misuse: 'Tool misuse detected {{count}} times for: {{tools}}. Verify tool schemas before calling.',
|
|
13
|
+
error_cascade: 'Repeated errors ({{count}}). Switch strategy: {{suggestion}}',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_THRESHOLDS = {
|
|
17
|
+
guardrail_spike: 3,
|
|
18
|
+
iteration_limit: 1,
|
|
19
|
+
tool_misuse: 2,
|
|
20
|
+
error_cascade: 3,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getStatePath(stateFile) {
|
|
24
|
+
return stateFile || DEFAULT_STATE_PATH;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadState(stateFile) {
|
|
28
|
+
const p = getStatePath(stateFile);
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
31
|
+
} catch {
|
|
32
|
+
// corrupted — start fresh
|
|
33
|
+
}
|
|
34
|
+
return { counts: {} };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveState(state, stateFile) {
|
|
38
|
+
const p = getStatePath(stateFile);
|
|
39
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
40
|
+
fs.writeFileSync(p, JSON.stringify(state, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Increment the event counter for a given event type.
|
|
45
|
+
* @param {string} eventType - One of the keys in REMINDER_TEMPLATES
|
|
46
|
+
* @param {string} [stateFile] - Path to state JSON (default: .rlhf/reminder-state.json)
|
|
47
|
+
* @returns {number} New count after incrementing
|
|
48
|
+
*/
|
|
49
|
+
function trackEvent(eventType, stateFile) {
|
|
50
|
+
const state = loadState(stateFile);
|
|
51
|
+
state.counts[eventType] = (state.counts[eventType] || 0) + 1;
|
|
52
|
+
saveState(state, stateFile);
|
|
53
|
+
return state.counts[eventType];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the current event count without modifying state.
|
|
58
|
+
* @param {string} eventType
|
|
59
|
+
* @param {string} [stateFile]
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function getEventCount(eventType, stateFile) {
|
|
63
|
+
return loadState(stateFile).counts[eventType] || 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return true if the event count meets or exceeds its threshold.
|
|
68
|
+
* @param {string} eventType
|
|
69
|
+
* @param {number} [threshold] - Defaults to DEFAULT_THRESHOLDS[eventType] or 3
|
|
70
|
+
* @param {string} [stateFile]
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function shouldInjectReminder(eventType, threshold, stateFile) {
|
|
74
|
+
const t = typeof threshold === 'number' ? threshold : (DEFAULT_THRESHOLDS[eventType] || 3);
|
|
75
|
+
return getEventCount(eventType, stateFile) >= t;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a reminder template with context variable substitution.
|
|
80
|
+
* @param {string} eventType
|
|
81
|
+
* @param {object} ctx - Variables to substitute into {{var}} placeholders
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function renderTemplate(eventType, ctx) {
|
|
85
|
+
const template = REMINDER_TEMPLATES[eventType];
|
|
86
|
+
if (!template) return `[Reminder] Event: ${eventType}`;
|
|
87
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (ctx && ctx[key] !== undefined ? ctx[key] : `{${key}}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Append a system reminder to a turns array without modifying state.
|
|
92
|
+
* Callers are responsible for calling trackEvent before/after as needed.
|
|
93
|
+
* @param {object[]} turns - Existing turns array
|
|
94
|
+
* @param {string} eventType
|
|
95
|
+
* @param {object} ctx - Template variables (count will be added automatically)
|
|
96
|
+
* @param {string} [stateFile]
|
|
97
|
+
* @returns {object[]} New turns array with reminder appended
|
|
98
|
+
*/
|
|
99
|
+
function injectReminder(turns, eventType, ctx, stateFile) {
|
|
100
|
+
const count = getEventCount(eventType, stateFile);
|
|
101
|
+
const message = renderTemplate(eventType, { ...ctx, count });
|
|
102
|
+
const reminder = {
|
|
103
|
+
role: 'user',
|
|
104
|
+
content: `[System Reminder] ${message}`,
|
|
105
|
+
injectedAt: new Date().toISOString(),
|
|
106
|
+
eventType,
|
|
107
|
+
};
|
|
108
|
+
return [...turns, reminder];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reset the event counter for a given event type (e.g., after a reminder is acted on).
|
|
113
|
+
* @param {string} eventType
|
|
114
|
+
* @param {string} [stateFile]
|
|
115
|
+
*/
|
|
116
|
+
function resetEvent(eventType, stateFile) {
|
|
117
|
+
const state = loadState(stateFile);
|
|
118
|
+
state.counts[eventType] = 0;
|
|
119
|
+
saveState(state, stateFile);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
REMINDER_TEMPLATES,
|
|
124
|
+
DEFAULT_THRESHOLDS,
|
|
125
|
+
DEFAULT_STATE_PATH,
|
|
126
|
+
trackEvent,
|
|
127
|
+
getEventCount,
|
|
128
|
+
shouldInjectReminder,
|
|
129
|
+
renderTemplate,
|
|
130
|
+
injectReminder,
|
|
131
|
+
resetEvent,
|
|
132
|
+
};
|