feat(context): add offline shadow profile and local dev gate
Memind CI / Test, build, and release guards (push) Has been cancelled
Memind CI / Test, build, and release guards (push) Has been cancelled
Provide context-runtime-profile simulation for gateway shadow events, run-context-runtime-offline.mjs for zero-LLM verification, and a local dev guide for user-led shadow self-tests later. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { buildHeadroomRunObservation, resolveHeadroomMode } from './memind-headroom-policy.mjs';
|
||||
import {
|
||||
buildContextBudgetResolvedEvent,
|
||||
planContextBudget,
|
||||
resolveContextBudgetMode,
|
||||
} from './context-budget.mjs';
|
||||
import { resolveMcpCompactMode } from './mcp-result-compactor.mjs';
|
||||
import {
|
||||
buildRecallFusionResolvedEvent,
|
||||
fuseRecallCandidates,
|
||||
resolveRecallFusionMode,
|
||||
} from './recall-fusion.mjs';
|
||||
import { resolveZvecWorkspaceMode } from './memind-zvec-workspace.mjs';
|
||||
|
||||
/** Recommended local shadow profile (observe-only, no behavior change). */
|
||||
export const CONTEXT_RUNTIME_SHADOW_ENV = Object.freeze({
|
||||
MEMIND_HEADROOM_MODE: 'shadow',
|
||||
MEMIND_CONTEXT_BUDGET_MODE: 'shadow',
|
||||
MEMIND_RECALL_FUSION_MODE: 'shadow',
|
||||
MEMIND_MCP_COMPACT_MODE: 'shadow',
|
||||
MEMIND_ZVEC_WORKSPACE_MODE: 'shadow',
|
||||
});
|
||||
|
||||
export function describeContextRuntimeProfile(env = process.env) {
|
||||
return {
|
||||
headroom: resolveHeadroomMode(env),
|
||||
contextBudget: resolveContextBudgetMode(env),
|
||||
recallFusion: resolveRecallFusionMode(env),
|
||||
mcpCompact: resolveMcpCompactMode(env),
|
||||
zvecWorkspace: resolveZvecWorkspaceMode(env),
|
||||
};
|
||||
}
|
||||
|
||||
export function isContextRuntimeShadowProfile(env = process.env) {
|
||||
const profile = describeContextRuntimeProfile(env);
|
||||
return Object.values(profile).every((mode) => mode === 'off' || mode === 'shadow');
|
||||
}
|
||||
|
||||
export function isAnyContextRuntimeObservabilityEnabled(env = process.env) {
|
||||
const profile = describeContextRuntimeProfile(env);
|
||||
return Object.values(profile).some((mode) => mode !== 'off');
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline simulation of gateway shadow events (no LLM / no DB).
|
||||
* Mirrors agent-run-gateway appendEvent payloads for local evidence review.
|
||||
*/
|
||||
export function simulateContextRuntimeShadowEvents({
|
||||
skillId = 'web',
|
||||
userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我查一下 Memind context runtime 设计' }],
|
||||
metadata: { displayText: '帮我查一下 Memind context runtime 设计' },
|
||||
},
|
||||
routing = {
|
||||
route: 'agent_orchestration',
|
||||
suggestedSkill: 'web',
|
||||
reason: 'offline-shadow-simulation',
|
||||
},
|
||||
memoryContext = {
|
||||
mode: 'shadow',
|
||||
injectionEnabled: true,
|
||||
memories: [
|
||||
{ label: '偏好', text: '用户偏好:喜欢简洁回答' },
|
||||
{ label: '偏好副本', text: '用户偏好:喜欢简洁回答' },
|
||||
{ label: '项目', text: '正在推进 Context Runtime 融合' },
|
||||
],
|
||||
},
|
||||
env = { ...CONTEXT_RUNTIME_SHADOW_ENV },
|
||||
} = {}) {
|
||||
const events = {};
|
||||
const resolvedSkill = routing?.suggestedSkill ?? routing?.suggested_skill ?? skillId;
|
||||
|
||||
if (resolveHeadroomMode(env) !== 'off') {
|
||||
events.headroom_context_observed = buildHeadroomRunObservation({
|
||||
skillId: resolvedSkill,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolveRecallFusionMode(env) !== 'off') {
|
||||
const fusion = fuseRecallCandidates({
|
||||
personalMemories: memoryContext?.memories ?? [],
|
||||
episodicMemories: [],
|
||||
temporalItems: [],
|
||||
query: userMessage.metadata?.displayText ?? 'context runtime',
|
||||
limit: 3,
|
||||
env,
|
||||
});
|
||||
events.recall_fusion_resolved = buildRecallFusionResolvedEvent(fusion);
|
||||
}
|
||||
|
||||
if (resolveContextBudgetMode(env) !== 'off') {
|
||||
const displayText = userMessage.metadata?.displayText
|
||||
?? userMessage.content?.find?.((item) => item?.type === 'text')?.text
|
||||
?? '';
|
||||
const budgetPlan = planContextBudget({
|
||||
userTask: displayText,
|
||||
skillPrompt: `[skill:${resolvedSkill}] 使用 web 搜索补充事实`,
|
||||
memories: memoryContext?.injectionEnabled ? memoryContext.memories : [],
|
||||
temporalText: '最近一周无日程冲突',
|
||||
harnessEntries: [{ title: 'TKMind 用户偏好画像', content: '用户偏好:喜欢简洁回答' }],
|
||||
}, { env });
|
||||
events.context_budget_resolved = buildContextBudgetResolvedEvent(budgetPlan);
|
||||
}
|
||||
|
||||
return {
|
||||
profile: describeContextRuntimeProfile(env),
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateContextRuntimeShadowEvents(simulation) {
|
||||
const issues = [];
|
||||
const { profile, events } = simulation ?? {};
|
||||
if (!profile || !events) {
|
||||
return { ok: false, issues: ['simulation missing profile or events'] };
|
||||
}
|
||||
|
||||
if (profile.headroom !== 'off' && !events.headroom_context_observed) {
|
||||
issues.push('headroom mode enabled but headroom_context_observed missing');
|
||||
}
|
||||
if (profile.contextBudget !== 'off' && !events.context_budget_resolved) {
|
||||
issues.push('context budget mode enabled but context_budget_resolved missing');
|
||||
}
|
||||
if (profile.recallFusion !== 'off' && !events.recall_fusion_resolved) {
|
||||
issues.push('recall fusion mode enabled but recall_fusion_resolved missing');
|
||||
}
|
||||
if (events.context_budget_resolved?.mode === 'shadow' && !events.context_budget_resolved?.duplicateCount) {
|
||||
issues.push('expected shadow budget simulation to report duplicate fingerprints');
|
||||
}
|
||||
|
||||
return { ok: issues.length === 0, issues };
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
CONTEXT_RUNTIME_SHADOW_ENV,
|
||||
describeContextRuntimeProfile,
|
||||
isContextRuntimeShadowProfile,
|
||||
simulateContextRuntimeShadowEvents,
|
||||
validateContextRuntimeShadowEvents,
|
||||
} from './context-runtime-profile.mjs';
|
||||
|
||||
test('describeContextRuntimeProfile defaults to off', () => {
|
||||
const profile = describeContextRuntimeProfile({});
|
||||
assert.equal(profile.headroom, 'off');
|
||||
assert.equal(profile.contextBudget, 'off');
|
||||
assert.equal(profile.recallFusion, 'off');
|
||||
});
|
||||
|
||||
test('isContextRuntimeShadowProfile accepts all-shadow bundle', () => {
|
||||
assert.equal(isContextRuntimeShadowProfile(CONTEXT_RUNTIME_SHADOW_ENV), true);
|
||||
assert.equal(isContextRuntimeShadowProfile({
|
||||
...CONTEXT_RUNTIME_SHADOW_ENV,
|
||||
MEMIND_HEADROOM_MODE: 'active',
|
||||
}), false);
|
||||
});
|
||||
|
||||
test('simulateContextRuntimeShadowEvents emits gateway-shaped payloads', () => {
|
||||
const simulation = simulateContextRuntimeShadowEvents();
|
||||
assert.ok(simulation.events.headroom_context_observed);
|
||||
assert.ok(simulation.events.context_budget_resolved);
|
||||
assert.ok(simulation.events.recall_fusion_resolved);
|
||||
assert.equal(simulation.events.headroom_context_observed.mode, 'shadow');
|
||||
assert.equal(simulation.events.context_budget_resolved.mode, 'shadow');
|
||||
const validation = validateContextRuntimeShadowEvents(simulation);
|
||||
assert.equal(validation.ok, true, validation.issues?.join('; '));
|
||||
});
|
||||
|
||||
test('simulateContextRuntimeShadowEvents respects off modes', () => {
|
||||
const simulation = simulateContextRuntimeShadowEvents({
|
||||
env: { MEMIND_HEADROOM_MODE: 'off', MEMIND_CONTEXT_BUDGET_MODE: 'off', MEMIND_RECALL_FUSION_MODE: 'off' },
|
||||
});
|
||||
assert.equal(simulation.events.headroom_context_observed, undefined);
|
||||
assert.equal(simulation.events.context_budget_resolved, undefined);
|
||||
assert.equal(simulation.events.recall_fusion_resolved, undefined);
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
# Context Runtime 本地离线开发
|
||||
|
||||
> 不调用 LLM、不跑 phase3 live smoke。用户自测见文末清单。
|
||||
|
||||
## 一键离线验证
|
||||
|
||||
```bash
|
||||
node scripts/run-context-runtime-offline.mjs
|
||||
node scripts/run-goosed-v149-phase3-offline.mjs # 含 v1.49 单测 + manifest drift
|
||||
```
|
||||
|
||||
通过时应分别输出 **`CONTEXT_RUNTIME_OFFLINE_OK`** 与 **`GOOSE_V149_PHASE3_OFFLINE_OK`**。
|
||||
|
||||
## 推荐 shadow 配置(仅观测,不改行为)
|
||||
|
||||
写入 `.env.local`(或临时 export):
|
||||
|
||||
```bash
|
||||
MEMIND_HEADROOM_MODE=shadow
|
||||
MEMIND_CONTEXT_BUDGET_MODE=shadow
|
||||
MEMIND_RECALL_FUSION_MODE=shadow
|
||||
MEMIND_MCP_COMPACT_MODE=shadow
|
||||
MEMIND_ZVEC_WORKSPACE_MODE=shadow
|
||||
HEADROOM_OUTPUT_SHAPER=0
|
||||
```
|
||||
|
||||
Bundle 常量见 `context-runtime-profile.mjs` → `CONTEXT_RUNTIME_SHADOW_ENV`。
|
||||
|
||||
## 离线事件形状预览
|
||||
|
||||
不启动 Portal 也可查看 gateway 将 emit 的 shadow 事件:
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
import { simulateContextRuntimeShadowEvents } from './context-runtime-profile.mjs';
|
||||
console.log(JSON.stringify(simulateContextRuntimeShadowEvents(), null, 2));
|
||||
"
|
||||
```
|
||||
|
||||
期望事件类型:
|
||||
|
||||
| event_type | 含义 |
|
||||
|---|---|
|
||||
| `headroom_context_observed` | headroom 路由/eligible 观测 |
|
||||
| `context_budget_resolved` | 注入预算 dedupe/drop 计划 |
|
||||
| `recall_fusion_resolved` | 多路 recall 融合统计 |
|
||||
|
||||
## 组件探针(可选)
|
||||
|
||||
```bash
|
||||
# headroom proxy 已起且 MEMIND_HEADROOM_MODE!=off
|
||||
node scripts/check-headroom-proxy-local.mjs
|
||||
|
||||
# .zvec-grep 已 index
|
||||
node scripts/check-zvec-workspace-local.mjs
|
||||
|
||||
# MCP compactor shadow/active 逻辑
|
||||
node scripts/check-mcp-compactor-local.mjs
|
||||
```
|
||||
|
||||
## 用户自测清单(有 LLM 配额时)
|
||||
|
||||
1. 确认 **未设置** `GOOSE_V149_ALLOW_REAL_LLM=1` 除非你明确要跑 live smoke
|
||||
2. 应用 shadow env,**重启 Portal**(`pnpm dev`)
|
||||
3. 发起 **一轮**普通 agent 对话(非 page-e2e)
|
||||
4. 查 DB 事件:
|
||||
|
||||
```sql
|
||||
SELECT event_type, data_json, created_at
|
||||
FROM h5_agent_run_events
|
||||
WHERE run_id = '<run_id>'
|
||||
AND event_type IN (
|
||||
'headroom_context_observed',
|
||||
'context_budget_resolved',
|
||||
'recall_fusion_resolved'
|
||||
)
|
||||
ORDER BY created_at;
|
||||
```
|
||||
|
||||
5. 确认 `mode=shadow` 且 **聊天行为与 off 时一致**
|
||||
6. 若要 active 晋升:逐项改 `shadow` → `active`,每次只改一个开关
|
||||
|
||||
## 禁止项
|
||||
|
||||
- Agent **不得**自动设置 `GOOSE_V149_ALLOW_REAL_LLM=1` 或循环跑 phase2/phase3
|
||||
- **禁止** `headroom wrap` / `headroom learn` / `HEADROOM_OUTPUT_SHAPER=1`
|
||||
- 页面生成 / Page Data 轮次首期 **排除** headroom active
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [context-runtime-harness-fusion-plan.md](./context-runtime-harness-fusion-plan.md)
|
||||
- [goose-v149-real-llm-gate.md](./goose-v149-real-llm-gate.md)
|
||||
@@ -56,3 +56,10 @@ MEMORY_CANDIDATE_ENABLED=0
|
||||
# MEMIND_MCP_COMPACT_BUDGET_CHARS=4000
|
||||
# MEMIND_MCP_COMPACT_TTL_SECONDS=3600
|
||||
# MEMIND_RUNTIME_REDIS_URL=redis://127.0.0.1:6379/0
|
||||
|
||||
# Context Runtime shadow observe-only bundle (see docs/context-runtime-local-dev.md)
|
||||
# MEMIND_HEADROOM_MODE=shadow
|
||||
# MEMIND_CONTEXT_BUDGET_MODE=shadow
|
||||
# MEMIND_RECALL_FUSION_MODE=shadow
|
||||
# MEMIND_MCP_COMPACT_MODE=shadow
|
||||
# MEMIND_ZVEC_WORKSPACE_MODE=shadow
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Context Runtime offline gate: unit tests + local probes, no LLM.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
CONTEXT_RUNTIME_SHADOW_ENV,
|
||||
describeContextRuntimeProfile,
|
||||
simulateContextRuntimeShadowEvents,
|
||||
validateContextRuntimeShadowEvents,
|
||||
} from '../context-runtime-profile.mjs';
|
||||
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
|
||||
import { checkPassed, classifyCheckResult } from './goose-v149-check-result.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const checkEnv = prepareGooseV149CheckEnv({ ...process.env }, root);
|
||||
|
||||
const unitChecks = [
|
||||
{
|
||||
name: 'context-runtime-profile',
|
||||
command: [process.execPath, '--test', 'context-runtime-profile.test.mjs'],
|
||||
},
|
||||
{
|
||||
name: 'context-runtime-modules',
|
||||
command: [
|
||||
process.execPath,
|
||||
'--test',
|
||||
'memind-headroom-policy.test.mjs',
|
||||
'context-budget.test.mjs',
|
||||
'recall-fusion.test.mjs',
|
||||
'mcp-result-compactor.test.mjs',
|
||||
'memind-zvec-workspace.test.mjs',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const scriptChecks = [
|
||||
{ name: 'mcp-compactor-local', script: 'check-mcp-compactor-local.mjs' },
|
||||
{ name: 'headroom-proxy-local', script: 'check-headroom-proxy-local.mjs', optional: true },
|
||||
{ name: 'zvec-workspace-local', script: 'check-zvec-workspace-local.mjs', optional: true },
|
||||
];
|
||||
|
||||
function runCommand(command) {
|
||||
const [bin, ...args] = command;
|
||||
const result = spawnSync(bin, args, {
|
||||
cwd: root,
|
||||
env: checkEnv,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return {
|
||||
ok: checkPassed(result),
|
||||
outcome: classifyCheckResult(result),
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function runScript(script) {
|
||||
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script)], {
|
||||
cwd: root,
|
||||
env: checkEnv,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return {
|
||||
ok: checkPassed(result),
|
||||
outcome: classifyCheckResult(result),
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('CONTEXT_RUNTIME_OFFLINE:');
|
||||
console.log(` profile=${JSON.stringify(describeContextRuntimeProfile(checkEnv))}`);
|
||||
console.log(` recommended_shadow=${JSON.stringify(CONTEXT_RUNTIME_SHADOW_ENV)}`);
|
||||
|
||||
const simulation = simulateContextRuntimeShadowEvents({ env: { ...CONTEXT_RUNTIME_SHADOW_ENV } });
|
||||
const validation = validateContextRuntimeShadowEvents(simulation);
|
||||
console.log(` shadow_simulation=${validation.ok ? 'OK' : 'FAIL'}`);
|
||||
if (!validation.ok) {
|
||||
console.error(validation.issues.join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` sample_events=${Object.keys(simulation.events).join(',')}`);
|
||||
|
||||
const failures = [];
|
||||
|
||||
for (const check of unitChecks) {
|
||||
const result = runCommand(check.command);
|
||||
console.log(`[context-runtime-offline] ${result.outcome} ${check.name}`);
|
||||
if (!result.ok) {
|
||||
failures.push(check.name);
|
||||
if (result.stderr) console.error(result.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
for (const check of scriptChecks) {
|
||||
const result = runScript(check.script);
|
||||
const skipped = /SKIP:/.test(result.stdout);
|
||||
if (skipped && check.optional) {
|
||||
console.log(`[context-runtime-offline] SKIP ${check.name}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`[context-runtime-offline] ${result.outcome} ${check.name}`);
|
||||
if (!result.ok && !check.optional) {
|
||||
failures.push(check.name);
|
||||
if (result.stderr) console.error(result.stderr);
|
||||
} else if (!result.ok && check.optional) {
|
||||
console.log(`[context-runtime-offline] WARN ${check.name} (optional probe failed)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`CONTEXT_RUNTIME_OFFLINE_FAIL: ${failures.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('CONTEXT_RUNTIME_OFFLINE_OK');
|
||||
console.log(' self-test: docs/context-runtime-local-dev.md');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -48,15 +48,8 @@ const checks = [
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'context-runtime-unit',
|
||||
command: [
|
||||
process.execPath,
|
||||
'--test',
|
||||
'memind-headroom-policy.test.mjs',
|
||||
'context-budget.test.mjs',
|
||||
'recall-fusion.test.mjs',
|
||||
'mcp-result-compactor.test.mjs',
|
||||
],
|
||||
name: 'context-runtime-offline',
|
||||
script: 'run-context-runtime-offline.mjs',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user