diff --git a/context-runtime-profile.mjs b/context-runtime-profile.mjs new file mode 100644 index 0000000..3b8aa83 --- /dev/null +++ b/context-runtime-profile.mjs @@ -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 }; +} diff --git a/context-runtime-profile.test.mjs b/context-runtime-profile.test.mjs new file mode 100644 index 0000000..94102cc --- /dev/null +++ b/context-runtime-profile.test.mjs @@ -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); +}); diff --git a/docs/context-runtime-local-dev.md b/docs/context-runtime-local-dev.md new file mode 100644 index 0000000..f4372a8 --- /dev/null +++ b/docs/context-runtime-local-dev.md @@ -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 = '' + 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) diff --git a/docs/goose-v149-canary.env.example b/docs/goose-v149-canary.env.example index 64ec03c..491b71b 100644 --- a/docs/goose-v149-canary.env.example +++ b/docs/goose-v149-canary.env.example @@ -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 diff --git a/scripts/run-context-runtime-offline.mjs b/scripts/run-context-runtime-offline.mjs new file mode 100644 index 0000000..6adf16f --- /dev/null +++ b/scripts/run-context-runtime-offline.mjs @@ -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(); diff --git a/scripts/run-goosed-v149-phase3-offline.mjs b/scripts/run-goosed-v149-phase3-offline.mjs index 6ef8602..bbb0731 100644 --- a/scripts/run-goosed-v149-phase3-offline.mjs +++ b/scripts/run-goosed-v149-phase3-offline.mjs @@ -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, }, ];