init: PrismFacet project plan
Dynamic system prompt injection plugin for OpenClaw. Routes agent context through configurable routers, matches against registered rules, and injects prompt files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
217
PLAN.md
Normal file
217
PLAN.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# PrismFacet
|
||||
|
||||
Dynamic system prompt injection plugin for OpenClaw. Routes agent context through configurable routers, matches against registered rules, and injects corresponding prompt files into the system prompt.
|
||||
|
||||
## Concept
|
||||
|
||||
```
|
||||
Agent session starts
|
||||
→ before_prompt_build(ctx)
|
||||
→ for each router:
|
||||
router.resolve(ctx) → key (e.g. "developer")
|
||||
lookup rules for router:key (e.g. "role:developer")
|
||||
read prompt file (e.g. roles/developer/ROLE.md)
|
||||
→ append all matched prompts to system prompt
|
||||
```
|
||||
|
||||
**Routers** resolve context into a key. They are TypeScript files — drop one into `routers/` to register it.
|
||||
|
||||
**Rules** map `router:key` → prompt file path. Managed via MCP tool at runtime.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
~/.openclaw/plugins/prism-facet/
|
||||
├── openclaw.plugin.json # Plugin manifest
|
||||
├── package.json
|
||||
├── index.ts # Entry point
|
||||
├── src/
|
||||
│ ├── router-loader.ts # Load/reload routers from routers/
|
||||
│ ├── rule-store.ts # CRUD for rules (rules.json)
|
||||
│ └── prompt-injector.ts # before_prompt_build hook logic
|
||||
├── routers/ # Router functions (drop-in)
|
||||
│ ├── role.ts # Resolves agent role (reads ego.json)
|
||||
│ ├── position.ts # Resolves agent position
|
||||
│ └── agent-id.ts # Returns agentId directly (no deps)
|
||||
└── rules.json # Persisted rules (managed via tool)
|
||||
```
|
||||
|
||||
## Router Interface
|
||||
|
||||
```typescript
|
||||
// Every router exports a resolve function
|
||||
export interface RouterContext {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export function resolve(ctx: RouterContext): string | Promise<string>;
|
||||
```
|
||||
|
||||
Example routers:
|
||||
|
||||
```typescript
|
||||
// routers/agent-id.ts — zero dependencies
|
||||
export function resolve(ctx: { agentId: string }): string {
|
||||
return ctx.agentId;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// routers/role.ts — reads ego.json (optional dependency on PaddedCell)
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const EGO_PATH = `${homedir()}/.openclaw/ego.json`;
|
||||
|
||||
export function resolve(ctx: { agentId: string }): string {
|
||||
try {
|
||||
const ego = JSON.parse(readFileSync(EGO_PATH, 'utf8'));
|
||||
return ego['agent-scope']?.[ctx.agentId]?.role || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hot Reload
|
||||
|
||||
Routers are loaded via dynamic `import()` with cache-busting. A reload tool clears the module cache and re-imports all routers without restarting the gateway.
|
||||
|
||||
## Rules
|
||||
|
||||
Rules are stored in `rules.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"role:developer": "/path/to/roles/developer/ROLE.md",
|
||||
"role:operator": "/path/to/roles/operator/ROLE.md",
|
||||
"position:tech-leader": "/path/to/positions/tech-leader/POSITION.md",
|
||||
"agent-id:operator": "/path/to/special/operator-extra.md"
|
||||
}
|
||||
```
|
||||
|
||||
Key format: `{router-name}:{resolved-key}`
|
||||
|
||||
## MCP Tools
|
||||
|
||||
### prompt-rules
|
||||
|
||||
| Subcommand | Description |
|
||||
|------------|-------------|
|
||||
| `add --router <name> --key <key> --file <path>` | Register a rule |
|
||||
| `remove --router <name> --key <key>` | Remove a rule |
|
||||
| `list` | List all rules |
|
||||
| `test --agent <agent-id>` | Preview which rules match and what prompts would be injected |
|
||||
| `reload-routers` | Hot-reload all router functions |
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
```
|
||||
1. Gateway starts → plugin loads
|
||||
2. Load all routers from routers/ via dynamic import()
|
||||
3. Load rules.json
|
||||
|
||||
On each before_prompt_build(event, ctx):
|
||||
4. For each loaded router:
|
||||
a. key = router.resolve(ctx)
|
||||
b. If key is empty → skip
|
||||
c. ruleKey = "{routerName}:{key}"
|
||||
d. If rules[ruleKey] exists → read the prompt file
|
||||
5. Concatenate all matched prompt contents
|
||||
6. Return { appendSystemContext: concatenated }
|
||||
|
||||
On reload-routers tool call:
|
||||
7. Clear import cache for all routers
|
||||
8. Re-import all routers from routers/
|
||||
```
|
||||
|
||||
## Plugin Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "prism-facet",
|
||||
"name": "PrismFacet",
|
||||
"version": "0.1.0",
|
||||
"description": "Dynamic system prompt injection via routers and rules",
|
||||
"main": "dist/index.js",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"routersDir": {
|
||||
"type": "string",
|
||||
"description": "Directory containing router .ts/.js files"
|
||||
},
|
||||
"rulesFile": {
|
||||
"type": "string",
|
||||
"description": "Path to rules.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```json
|
||||
// openclaw.json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"prism-facet": {
|
||||
"enabled": true,
|
||||
"hooks": {
|
||||
"allowPromptInjection": true
|
||||
},
|
||||
"config": {
|
||||
"routersDir": "~/.openclaw/plugins/prism-facet/routers",
|
||||
"rulesFile": "~/.openclaw/plugins/prism-facet/rules.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Plugin
|
||||
|
||||
- [ ] Plugin scaffold (manifest, package.json, tsconfig)
|
||||
- [ ] Router loader with dynamic import + cache busting
|
||||
- [ ] Rule store (CRUD on rules.json)
|
||||
- [ ] before_prompt_build hook: resolve routers → match rules → inject prompts
|
||||
- [ ] Built-in routers: agent-id.ts
|
||||
|
||||
### Phase 2: MCP Tools
|
||||
|
||||
- [ ] `prompt-rules add/remove/list` tool
|
||||
- [ ] `prompt-rules test` tool (preview matches for an agent)
|
||||
- [ ] `prompt-rules reload-routers` tool
|
||||
|
||||
### Phase 3: Default Routers
|
||||
|
||||
- [ ] role.ts (reads ego.json)
|
||||
- [ ] position.ts (reads ego.json)
|
||||
- [ ] Documentation for writing custom routers
|
||||
|
||||
### Phase 4: Testing & Deployment
|
||||
|
||||
- [ ] Test on laptop environment
|
||||
- [ ] Deploy to T2 (production)
|
||||
- [ ] Integrate with ClawSkills role/position definitions
|
||||
- [ ] Verify injection for all active agents
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Required? | Notes |
|
||||
|-----------|-----------|-------|
|
||||
| OpenClaw v2026.4.9+ | Yes | before_prompt_build + allowPromptInjection |
|
||||
| PaddedCell / ego-mgr | No | Only needed by role.ts / position.ts routers |
|
||||
| Node.js ESM | Yes | Dynamic import() for router loading |
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **No hard dependencies** — Plugin works with just agent-id router. External system routers are optional.
|
||||
2. **Drop-in extensibility** — Add a .ts file to routers/ to add a new routing dimension.
|
||||
3. **Hot reload** — Router changes don't require gateway restart.
|
||||
4. **Separation of concerns** — Routers resolve context → Rules map keys to files → Plugin injects content.
|
||||
Reference in New Issue
Block a user