mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 23:49:40 +02:00
Three Claude-Code-inspired primitives for runPlannerLoop, derived from the
reverse-engineering reports in docs/reports/:
1. **Policy gate** (@mana/tool-registry) — evaluatePolicy() gates every tool
dispatch: denies admin-scope, denies destructive tools not in the user's
opt-in list, rate-limits per tool (30/60s default), flags prompt-injection
markers in freetext without blocking. Wired into mana-mcp with a
per-user rolling invocation log and POLICY_MODE env (off|log-only|enforce,
default log-only). mana-ai uses detectInjectionMarker only — tool dispatch
there is plan-only, so rate-limit/destructive checks don't apply yet.
2. **Reminder channel** (packages/shared-ai/src/planner/loop.ts) — new
reminderChannel callback in PlannerLoopInput. Called once per round with
LoopState snapshot (round, toolCallCount, usage, lastCall); returned
strings wrap in <reminder> tags and inject as transient system messages
into THIS LLM request only. Never pushed to messages[] — the Claude-Code
<system-reminder> pattern that keeps the KV-cache prefix stable.
3. **Parallel reads** (loop.ts) — isParallelSafe predicate enables
Promise.all dispatch when every tool_call in a round is parallel-safe,
in batches of PARALLEL_TOOL_BATCH_SIZE=10. Any non-safe call downgrades
the whole round to sequential. messages[] always appends in source
order, never completion order, so the debug log stays linear.
Default-off (undefined predicate) preserves pre-M1 behaviour.
Tests: 21 new in tool-registry (policy), 9 new in shared-ai (5 parallel,
4 reminder). All 74 green, type-check clean across 4 packages.
Design/plan: docs/plans/agent-loop-improvements-m1.md
Reports: docs/reports/claude-code-architecture.md,
docs/reports/mana-agent-improvements-from-claude-code.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
46 lines
1.5 KiB
TypeScript
46 lines
1.5 KiB
TypeScript
/**
|
|
* Per-user rolling invocation log, consumed by the policy gate's
|
|
* rate-limiter. Pure in-memory — sessions are per-process in mana-mcp
|
|
* and the rate-limit window is short (60s), so persistence is pointless.
|
|
*
|
|
* Each user gets their own ring buffer capped at `MAX_EVENTS`. We prune
|
|
* older-than-window events opportunistically on every `append`, so the
|
|
* buffer stays small.
|
|
*/
|
|
|
|
import { RATE_LIMIT_WINDOW_MS, type InvocationEvent } from '@mana/tool-registry';
|
|
|
|
const MAX_EVENTS_PER_USER = 512;
|
|
|
|
const logs = new Map<string, InvocationEvent[]>();
|
|
|
|
export function appendInvocation(userId: string, toolName: string, at: number = Date.now()): void {
|
|
let events = logs.get(userId);
|
|
if (!events) {
|
|
events = [];
|
|
logs.set(userId, events);
|
|
}
|
|
events.push({ toolName, at });
|
|
|
|
// Drop events outside the window. Done in-place; O(n) per append is
|
|
// acceptable at our event rates.
|
|
const cutoff = at - RATE_LIMIT_WINDOW_MS;
|
|
while (events.length > 0 && events[0].at < cutoff) {
|
|
events.shift();
|
|
}
|
|
|
|
// Hard ceiling — protects against a burst-and-disconnect session that
|
|
// would otherwise accumulate forever between periodic cleanups.
|
|
if (events.length > MAX_EVENTS_PER_USER) {
|
|
events.splice(0, events.length - MAX_EVENTS_PER_USER);
|
|
}
|
|
}
|
|
|
|
export function getRecentInvocations(userId: string): readonly InvocationEvent[] {
|
|
return logs.get(userId) ?? [];
|
|
}
|
|
|
|
/** Test-only — the log is a module-level singleton otherwise. */
|
|
export function __resetInvocationLogForTests(): void {
|
|
logs.clear();
|
|
}
|