Skip to content

Commit 6b01908

Browse files
committed
Add hooks API proposal with trajectory integration
Port the hooks API design document from PR #8 with additional trajectory integration examples showing how hooks can work with the PDERO paradigm and trail CLI.
1 parent 6eabbf5 commit 6b01908

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed

docs/HOOKS_API.md

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
# Agent Relay Hooks API
2+
3+
**Date:** 2025-12-21
4+
**Status:** Proposed
5+
6+
## Overview
7+
8+
Hooks are a core primitive in agent-relay that allow:
9+
1. **Intercepting agent output** - React to patterns, events, session lifecycle
10+
2. **Injecting prompts** - Guide agent behavior automatically
11+
3. **Extending with namespaces** - User-defined `@pattern:` handlers
12+
13+
## Pattern Namespaces
14+
15+
agent-relay intercepts output patterns in the format `@namespace:target message`.
16+
17+
### Built-in Namespaces
18+
19+
| Namespace | Purpose | Example |
20+
|-----------|---------|---------|
21+
| `@relay:` | Inter-agent messaging | `@relay:Alice Check the tests` |
22+
| `@memory:` | Memory operations | `@memory:save User prefers dark mode` |
23+
| `@broadcast:` | Broadcast to all | `@relay:* Status update` |
24+
25+
### Memory Namespace
26+
27+
```
28+
@memory:save <content> # Store a memory
29+
@memory:search <query> # Retrieve relevant memories
30+
@memory:forget <id> # Delete a memory
31+
@memory:list # List recent memories
32+
```
33+
34+
### User-Defined Namespaces
35+
36+
Users can register custom pattern handlers:
37+
38+
```typescript
39+
// relay.config.ts
40+
export default {
41+
patterns: {
42+
// Custom namespace: @deploy:
43+
deploy: {
44+
handler: async (target, message, context) => {
45+
if (target === 'staging') {
46+
await exec('npm run deploy:staging');
47+
return { inject: 'Deployed to staging successfully' };
48+
}
49+
}
50+
},
51+
52+
// Custom namespace: @notify:
53+
notify: {
54+
handler: async (target, message, context) => {
55+
await fetch('https://slack.com/api/post', {
56+
body: JSON.stringify({ channel: target, text: message })
57+
});
58+
}
59+
}
60+
}
61+
};
62+
```
63+
64+
Usage in agent output:
65+
```
66+
@deploy:staging Release v1.2.3
67+
@notify:#engineering Build complete
68+
```
69+
70+
## Hook Lifecycle
71+
72+
```
73+
┌─────────────────────────────────────────────────────────────────────────────┐
74+
│ HOOK LIFECYCLE │
75+
│ │
76+
│ SESSION START │
77+
│ │ │
78+
│ ▼ │
79+
│ ┌─────────────────┐ │
80+
│ │ onSessionStart │ → Inject initial context, load memories │
81+
│ └────────┬────────┘ │
82+
│ │ │
83+
│ ▼ │
84+
│ ┌─────────────────────────────────────────────────────────────────┐ │
85+
│ │ AGENT RUNNING │ │
86+
│ │ │ │
87+
│ │ Agent Output ──► onOutput ──► Pattern Match? ──► Handler │ │
88+
│ │ │ │ │ │
89+
│ │ │ ▼ │ │
90+
│ │ │ @relay: → route message │ │
91+
│ │ │ @memory: → store/search │ │
92+
│ │ │ @custom: → user handler │ │
93+
│ │ │ │ │
94+
│ │ ▼ │ │
95+
│ │ onToolCall ──► Before/after tool execution │ │
96+
│ │ │ │ │
97+
│ │ ▼ │ │
98+
│ │ onMessageReceived ──► Inject incoming relay messages │ │
99+
│ │ │ │ │
100+
│ │ ▼ │ │
101+
│ │ onIdle ──► Periodic prompts (memory review, status) │ │
102+
│ │ │ │
103+
│ └─────────────────────────────────────────────────────────────────┘ │
104+
│ │ │
105+
│ ▼ │
106+
│ ┌─────────────────┐ │
107+
│ │ onSessionEnd │ → Prompt for memory save, cleanup │
108+
│ └─────────────────┘ │
109+
│ │
110+
└─────────────────────────────────────────────────────────────────────────────┘
111+
```
112+
113+
## Hook API
114+
115+
### Configuration File
116+
117+
```typescript
118+
// relay.config.ts (in project root or ~/.config/agent-relay/)
119+
import type { RelayConfig } from 'agent-relay';
120+
121+
export default {
122+
// Pattern handlers (namespaces)
123+
patterns: {
124+
memory: 'builtin', // Use built-in memory handler
125+
deploy: { handler: myDeployHandler },
126+
},
127+
128+
// Lifecycle hooks
129+
hooks: {
130+
onSessionStart: async (ctx) => {
131+
// Load relevant memories
132+
const memories = await ctx.memory.search(ctx.workingDir);
133+
return { inject: `Relevant context:\n${memories}` };
134+
},
135+
136+
onSessionEnd: async (ctx) => {
137+
return {
138+
inject: `Session ending. Save any important learnings with @memory:save`
139+
};
140+
},
141+
142+
onOutput: async (output, ctx) => {
143+
// Custom output processing
144+
if (output.includes('ERROR')) {
145+
await ctx.notify('errors', output);
146+
}
147+
},
148+
149+
onIdle: async (ctx) => {
150+
// Called after 30s of no output
151+
// Could prompt for status update
152+
},
153+
},
154+
155+
// Memory configuration
156+
memory: {
157+
backend: 'mem0', // or 'qdrant', 'custom'
158+
autoSave: false, // Don't auto-extract, let agent decide
159+
promptOnEnd: true, // Prompt to save at session end
160+
},
161+
} satisfies RelayConfig;
162+
```
163+
164+
### Programmatic API
165+
166+
```typescript
167+
import { Relay } from 'agent-relay';
168+
169+
const relay = new Relay({
170+
name: 'MyAgent',
171+
});
172+
173+
// Register pattern handler
174+
relay.pattern('deploy', async (target, message, ctx) => {
175+
console.log(`Deploying to ${target}: ${message}`);
176+
await deploy(target);
177+
return { inject: `Deployed to ${target}` };
178+
});
179+
180+
// Register lifecycle hook
181+
relay.on('sessionStart', async (ctx) => {
182+
const memories = await loadMemories(ctx.agentId);
183+
ctx.inject(`Your memories:\n${memories}`);
184+
});
185+
186+
relay.on('sessionEnd', async (ctx) => {
187+
ctx.inject('Save important learnings with @memory:save');
188+
});
189+
190+
// Start with wrapped command
191+
relay.wrap('claude');
192+
```
193+
194+
### Hook Context
195+
196+
```typescript
197+
interface HookContext {
198+
// Agent info
199+
agentId: string;
200+
agentName: string;
201+
sessionId: string;
202+
203+
// Environment
204+
workingDir: string;
205+
env: Record<string, string>;
206+
207+
// Actions
208+
inject(text: string): void; // Inject text to agent stdin
209+
send(to: string, msg: string): void; // Send relay message
210+
211+
// Built-in services
212+
memory: MemoryService; // Memory operations
213+
relay: RelayService; // Messaging operations
214+
215+
// Session state
216+
output: string[]; // All output so far
217+
messages: Message[]; // All relay messages
218+
}
219+
```
220+
221+
## Built-in Pattern Handlers
222+
223+
### @relay: (Messaging)
224+
225+
```typescript
226+
// Built-in, always available
227+
relay.pattern('relay', async (target, message, ctx) => {
228+
if (target === '*') {
229+
await ctx.relay.broadcast(message);
230+
} else {
231+
await ctx.relay.send(target, message);
232+
}
233+
});
234+
```
235+
236+
### @memory: (Memory Operations)
237+
238+
```typescript
239+
// Built-in when memory is configured
240+
relay.pattern('memory', async (action, content, ctx) => {
241+
switch (action) {
242+
case 'save':
243+
await ctx.memory.add(content, { agentId: ctx.agentId });
244+
return { inject: `✓ Saved to memory` };
245+
246+
case 'search':
247+
const results = await ctx.memory.search(content);
248+
return { inject: `Memories:\n${format(results)}` };
249+
250+
case 'forget':
251+
await ctx.memory.delete(content);
252+
return { inject: `✓ Forgotten` };
253+
}
254+
});
255+
```
256+
257+
## Example: Full Memory Integration
258+
259+
```typescript
260+
// relay.config.ts
261+
export default {
262+
patterns: {
263+
memory: 'builtin',
264+
},
265+
266+
hooks: {
267+
onSessionStart: async (ctx) => {
268+
// Search for relevant context based on current directory/project
269+
const projectMemories = await ctx.memory.search(
270+
`project: ${ctx.workingDir}`
271+
);
272+
const userPrefs = await ctx.memory.search('user preferences');
273+
274+
if (projectMemories.length || userPrefs.length) {
275+
return {
276+
inject: `
277+
[CONTEXT FROM MEMORY]
278+
${projectMemories.map(m => `- ${m.content}`).join('\n')}
279+
280+
[USER PREFERENCES]
281+
${userPrefs.map(m => `- ${m.content}`).join('\n')}
282+
`
283+
};
284+
}
285+
},
286+
287+
onSessionEnd: async (ctx) => {
288+
return {
289+
inject: `
290+
[SESSION ENDING]
291+
If you learned anything important, save it:
292+
@memory:save <what you learned>
293+
294+
Examples:
295+
@memory:save User prefers TypeScript over JavaScript
296+
@memory:save This project uses Prisma for database access
297+
@memory:save Auth tokens stored in httpOnly cookies
298+
`
299+
};
300+
},
301+
},
302+
303+
memory: {
304+
backend: 'mem0',
305+
config: {
306+
vectorStore: { provider: 'qdrant', url: 'http://localhost:6333' },
307+
embedder: { provider: 'ollama', model: 'nomic-embed-text' },
308+
},
309+
},
310+
};
311+
```
312+
313+
## Trajectory Integration
314+
315+
Hooks integrate naturally with trajectory tracking (PDERO paradigm):
316+
317+
```typescript
318+
// relay.config.ts
319+
export default {
320+
hooks: {
321+
onSessionStart: async (ctx) => {
322+
// Auto-start trajectory if task is provided
323+
if (ctx.task) {
324+
await exec(`trail start "${ctx.task}" --agent ${ctx.agentName}`);
325+
}
326+
327+
// Load active trajectory context
328+
const status = await exec('trail status --json');
329+
if (status.active) {
330+
return {
331+
inject: `Active trajectory: ${status.task}\nPhase: ${status.phase}`
332+
};
333+
}
334+
},
335+
336+
onOutput: async (output, ctx) => {
337+
// Auto-detect PDERO phase transitions
338+
const phases = ['planning', 'designing', 'implementing', 'testing', 'observing'];
339+
for (const phase of phases) {
340+
if (output.toLowerCase().includes(phase)) {
341+
await exec(`trail phase ${phase.slice(0, -3)} --reason "Auto-detected"`);
342+
break;
343+
}
344+
}
345+
},
346+
347+
onMessageReceived: async (from, body, ctx) => {
348+
// Record message in trajectory
349+
await exec(`trail event "Message from ${from}" --type observation`);
350+
},
351+
352+
onSessionEnd: async (ctx) => {
353+
return {
354+
inject: `
355+
[SESSION ENDING]
356+
Complete your trajectory with learnings:
357+
trail complete --summary "What you accomplished" --confidence 0.9
358+
`
359+
};
360+
},
361+
},
362+
};
363+
```
364+
365+
## Escaping Patterns
366+
367+
To output literal `@namespace:` without triggering handlers:
368+
369+
```
370+
\@relay:AgentName # Outputs literally, not routed
371+
\\@relay:AgentName # Outputs \@relay:AgentName
372+
```
373+
374+
## Priority & Order
375+
376+
1. Patterns are matched in order of specificity
377+
2. Built-in patterns run before user patterns (unless overridden)
378+
3. Multiple handlers for same pattern run in registration order
379+
4. Return `{ stop: true }` to prevent further handlers
380+
381+
## Next Steps
382+
383+
1. Implement pattern registry in agent-relay daemon
384+
2. Add hook lifecycle events to wrapper
385+
3. Implement @memory: built-in handler
386+
4. Create relay.config.ts loader
387+
5. Add trajectory hooks integration
388+
6. Add documentation and examples
389+
390+
## Related
391+
392+
- [MEMORY_STACK_DECISION.md](./MEMORY_STACK_DECISION.md) - Memory backend choice
393+
- [FEDERATION_PROPOSAL.md](./FEDERATION_PROPOSAL.md) - Cross-server messaging
394+
- [PROPOSAL-trajectories.md](./PROPOSAL-trajectories.md) - Trajectory tracking

0 commit comments

Comments
 (0)