feat(rules): acp_rule persistent rules — reconciled API (state, limits, hard protection) - #332
Conversation
…s, hard protection) Reconciles #283/#284 into one implementation matching the concrete consumer contract (billion-context#750 rules-feature.ts): - src/rules.ts: RULE_TOOL_NAME, DEFAULT_RULE_LIMITS {maxRules:50, maxRuleChars:300}, RULES_USAGE_PROMPT, listRules, allocateRuleId (pure, never-reuse, hand-crafted-state guard from #284), resolveRuleLimits(config) (Config.rules overrides, from #283), addRule/removeRule/clearRules — state-mutating, returning {ok:true;rule}|{ok:false;error} (host executeRule discards returned state), formatRulesForPrompt (system-prompt section, '' when empty), formatRulesList (tool-result rendering) - types: RuleRecord, CompressionState.rules?/nextRuleId? (optional, pre-rules states load unchanged), Config.rules? RulesConfig {enabled?,maxRules?,maxRuleChars?} — deviates from #282's original 'no Config field' stance because the host gates on config.rules.enabled and passes the resolved Config through - config: validateConfig rejects rules.maxRules/maxRuleChars < 1 - protected: ALWAYS_PROTECTED_TOOLS += acp_rule (unconditional, side-effect-free when the adapter never emits acp_rule messages) - state/compress(cloneState)/sync(state carry)/persist(state-merge fallbacks): rules + nextRuleId flow through every state copy site - tests/rules.test.ts: 15 tests incl. compression-exclusion integration (direct+effective coverage) and state-merge fallbacks Closes #282. Supersedes #283 and #284 (competing implementations of the same spec — one of them must be closed in favor of this one).
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)In your adapter project: npm install acp-kernel@pr-332Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf acp-kernel-pr332.tgz
npm install ./packageThis comment is automatically updated on each push. |
[bot] 🏷 PR #332 review — verified, ready for human merge. This is a completed agent PR ( Branch / CI
Code audit (diff vs base: 10 files, +431/−2, every hunk feature-relevant)
Non-blocking observations (flagging only — CI is green, no changes pushed)
Related threads: #282 (spec) closes on merge; #283 / #284 remain open — closing them in favor of this PR plus the billion-context pin bump are the listed post-merge follow-ups. Merge itself stays human-only per repo rules: #332 中文摘要:审查了 PR #332——代码已核对(API 与消费者端 #750 的导入完全对齐,状态流转四个构造点全覆盖,硬保护含 Bug-39 有效覆盖测试),CI 全绿且本地复跑 861/861 通过;仅发现一处非阻塞问题(id 分配表达式在 allocateRuleId/addRule 两处重复,建议抽公共函数防漂移),可以合并。 |
Closes #282. Supersedes #283 and #284 — they are competing implementations of the same spec (6 shared files, mutual conflict); one must win and this branch reconciles them against the only concrete consumer code that exists today.
Why a third branch
#283 and #284 agree on the core (state field, helpers,
ALWAYS_PROTECTED_TOOLS += acp_rule) but diverge on API shape — and neither matches billion-context#750 (the port of the feature origin billion-context-pi#433), whosesrc/rules-feature.tsimportsaddRule, listRules, formatRulesList, resolveRuleLimitsfrom the kernel:Config.rules(configurable)resolveRuleLimits(config)mergingConfig.rulesover defaultsmaxRules/maxRuleCharsmaxCount/maxCharsmaxRules/maxRuleCharsformatRulesForPromptonly)formatRulesForPromptformatRulesForPrompt+formatRulesList(rules)nextRuleIdmonotonic + never-reuse guardThe in-place mutation choice is deliberate: hosts hold one live
CompressionStateper session and #750'sexecuteRulecallsaddRule(state, text, limits)and discards any returned state — an immutable-only helper would silently lose the rule.Changes
src/rules.ts(new):RULE_TOOL_NAME,DEFAULT_RULE_LIMITS({maxRules: 50, maxRuleChars: 300}, frozen),RULES_USAGE_PROMPT,listRules,allocateRuleId(pure, never-reuse, hand-crafted-state backfill),resolveRuleLimits(config?),addRule/removeRule/clearRules(validating: non-empty → per-rule char cap → identical-content dedup → count cap),formatRulesForPrompt(""when empty — zero token cost),formatRulesListsrc/types.ts:RuleRecord {id, text},CompressionState.rules?+nextRuleId?(optional — pre-rules persisted states load unchanged),Config.rules?: RulesConfig {enabled?, maxRules?, maxRuleChars?}. Documented deviation from acp_rule 持久规则:状态字段、辅助函数与硬保护(跨仓功能内核侧) #282's original "no Config field" stance: the host gates onconfig.rules.enabled === trueand carries limit overrides through the resolved Config.src/config.ts:validateConfigrejectsrules.maxRules < 1/rules.maxRuleChars < 1src/protected.ts:ALWAYS_PROTECTED_TOOLS = ["compress", "acp_rule"]— unconditional; side-effect-free when the adapter never emits acp_rule messages (feature flag defaults off in pi acp.json / bili three-level config)src/state.ts,src/compress.ts(cloneState),src/sync.ts,src/persist/state-merge.ts:rules+nextRuleIdflow through every state-copy/merge site with fresh-state fallbackssrc/index.ts: barrel exportsACP_TOOL_NAMES(kernel-side wire never injects it — opt-in adapter surface, same stance as acp_absorb)Pre-flight
npm run typecheck: PASSnpm test: 861/861 (846 existing + 15 new intests/rules.test.ts: mutation+id sequence, empty/over-length/duplicate/cap rejections, never-reuse after remove/clear, hand-crafted-state backfill, resolveRuleLimits defaults/overrides, renderers, validateConfig, compression-exclusion integration (direct + effective coverage, Bug-39 shape), state-merge fallbacks)npm run build: PASSFollow-ups (after this merges + releases)
src/rules-feature.tsthen compiles as-is (imports already match this API).