Skip to content

Commit 3777f64

Browse files
authored
Merge pull request #1 from DrDroidLab/feature/top_level_agent
feat: Implement Vitest unit tests across agent modules and introduce …
2 parents 82057e5 + 53c281f commit 3777f64

16 files changed

Lines changed: 3807 additions & 1 deletion

.github/workflows/test.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: '22'
17+
cache: 'npm'
18+
cache-dependency-path: droid-agent/app/package-lock.json
19+
20+
- name: Install dependencies
21+
working-directory: droid-agent/app
22+
run: npm ci
23+
24+
- name: Run tests
25+
working-directory: droid-agent/app
26+
run: npm test

droid-agent/AGENT.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# AGENT.md — Droid Agent Knowledge Index
2+
3+
> This file is the master index of everything the agent knows. It is loaded before every request so the agent can quickly decide where to look and what context is available. Every update to memory, skills, incidents, infra, or learned patterns must update this file.
4+
5+
---
6+
7+
## Skills (7 files)
8+
9+
Domain knowledge and runbooks the agent uses during investigations.
10+
11+
| File | Size | First line |
12+
|------|------|------------|
13+
| `skills/docker.md` | 3706b | ```bash |
14+
| `skills/error-rate.md` | 2591b | When error rates spike, work through this in order: |
15+
| `skills/general-debugging.md` | 5554b | When something breaks, answer these questions in order: |
16+
| `skills/infra-sync.md` | 2157b | Discover everything about the developer's local and cloud infrastructure. Write |
17+
| `skills/kubernetes-incidents.md` | 5085b | Run these immediately when something is wrong in a k8s cluster: |
18+
| `skills/kubernetes.md` | 4165b | ```bash |
19+
| `skills/performance.md` | 3781b | When latency spikes or performance degrades: |
20+
21+
## Memory
22+
23+
### context.md
24+
<!-- Edit this file with permanent facts about your infrastructure -->
25+
26+
### README.md
27+
Persistent knowledge the agent reads on every message. Survives container restar
28+
29+
---
30+
31+
## How to use this index
32+
33+
- **Before investigating**: Check `infra/summary.md` and `context.md` for known infrastructure topology
34+
- **For k8s issues**: Read `skills/kubernetes-incidents.md` + `infra/kubernetes.md`
35+
- **For Docker issues**: Read `skills/docker.md` + `infra/docker.md`
36+
- **For performance**: Read `skills/performance.md`
37+
- **For error spikes**: Read `skills/error-rate.md`
38+
- **For past incidents**: Check `incidents/` for similar issues and `learned/patterns.md` for known investigation flows
39+
- **For general triage**: Read `skills/general-debugging.md`
40+
41+
---
42+
43+
*Last updated: 2026-03-18T12:24:15.792Z*

droid-agent/app/agent-md.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { readFile, writeFile } from 'fs/promises';
2+
import { readAllMemory } from './memory.js';
3+
import { loadSkills } from './skills.js';
4+
5+
const AGENT_MD_PATH = '/app/AGENT.md';
6+
7+
export async function regenerateAgentMd() {
8+
const skills = await loadSkills();
9+
const memoryFiles = await readAllMemory();
10+
11+
// Group memory files by directory
12+
const groups = {};
13+
for (const f of memoryFiles) {
14+
const slashIdx = f.path.indexOf('/');
15+
const dir = slashIdx !== -1 ? f.path.substring(0, slashIdx) : '/';
16+
if (!groups[dir]) groups[dir] = [];
17+
groups[dir].push(f);
18+
}
19+
20+
let md = `# AGENT.md — Droid Agent Knowledge Index
21+
22+
> This file is the master index of everything the agent knows. It is loaded before every request so the agent can quickly decide where to look and what context is available. Every update to memory, skills, incidents, infra, or learned patterns must update this file.
23+
24+
---
25+
26+
## Skills (${skills.length} files)
27+
28+
Domain knowledge and runbooks the agent uses during investigations.
29+
30+
| File | Size | First line |
31+
|------|------|------------|
32+
`;
33+
34+
for (const skill of skills) {
35+
const firstLine = (skill.content.split('\n').find(l => l.trim() && !l.startsWith('#')) || '').substring(0, 80);
36+
md += `| \`skills/${skill.name}.md\` | ${skill.size}b | ${firstLine} |\n`;
37+
}
38+
39+
md += `\n## Memory\n\n`;
40+
41+
// Root files
42+
if (groups['/']) {
43+
for (const f of groups['/']) {
44+
const firstLine = (f.content.split('\n').find(l => l.trim() && !l.startsWith('#')) || '').substring(0, 80);
45+
md += `### ${f.path}\n${firstLine}\n\n`;
46+
}
47+
}
48+
49+
// Directory groups
50+
const dirOrder = Object.keys(groups).filter(d => d !== '/').sort();
51+
for (const dir of dirOrder) {
52+
const files = groups[dir];
53+
md += `### ${dir}/ (${files.length} files)\n\n`;
54+
md += `| File | Size | Summary |\n|------|------|---------|\n`;
55+
for (const f of files) {
56+
const fileName = f.path.substring(dir.length + 1);
57+
// Extract first meaningful line or heading
58+
const lines = f.content.split('\n');
59+
let summary = '';
60+
for (const line of lines) {
61+
const trimmed = line.trim();
62+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('>') && !trimmed.startsWith('---') && !trimmed.startsWith('|')) {
63+
summary = trimmed.substring(0, 100);
64+
break;
65+
}
66+
}
67+
md += `| \`${dir}/${fileName}\` | ${f.size}b | ${summary} |\n`;
68+
}
69+
md += `\n`;
70+
}
71+
72+
md += `---
73+
74+
## How to use this index
75+
76+
- **Before investigating**: Check \`infra/summary.md\` and \`context.md\` for known infrastructure topology
77+
- **For k8s issues**: Read \`skills/kubernetes-incidents.md\` + \`infra/kubernetes.md\`
78+
- **For Docker issues**: Read \`skills/docker.md\` + \`infra/docker.md\`
79+
- **For performance**: Read \`skills/performance.md\`
80+
- **For error spikes**: Read \`skills/error-rate.md\`
81+
- **For past incidents**: Check \`incidents/\` for similar issues and \`learned/patterns.md\` for known investigation flows
82+
- **For general triage**: Read \`skills/general-debugging.md\`
83+
84+
---
85+
86+
*Last updated: ${new Date().toISOString()}*
87+
`;
88+
89+
await writeFile(AGENT_MD_PATH, md, 'utf-8');
90+
console.log(`[agent-md] Regenerated AGENT.md (${md.length} chars, ${skills.length} skills, ${memoryFiles.length} memory files)`);
91+
return md;
92+
}

droid-agent/app/agent.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
import { readFile } from 'fs/promises';
12
import { loadSkills, loadSkill } from './skills.js';
23
import { readAllMemory, readMemory, writeMemory } from './memory.js';
34
import { loadTools, loadMcpTools, executeTool } from './tools.js';
45
import { getConversation, saveConversation } from './redis.js';
56
import { logToolExecution, logIncident, upsertConversation, saveMessage, getRecentFeedback } from './db.js';
67
import { createCompletion, buildImageContent, getProviderName, getModelId } from './provider.js';
8+
import { regenerateAgentMd } from './agent-md.js';
9+
10+
const AGENT_MD_PATH = '/app/AGENT.md';
11+
12+
async function loadAgentMd() {
13+
try {
14+
return await readFile(AGENT_MD_PATH, 'utf-8');
15+
} catch {
16+
return null;
17+
}
18+
}
719

820
const MAX_TOOL_ITERATIONS = 10;
921
const MAX_SYNC_ITERATIONS = 40;
@@ -25,6 +37,15 @@ function log(emitter, type, data) {
2537
}
2638

2739
async function buildSystemPrompt(emitter) {
40+
// Load AGENT.md first — the master knowledge index
41+
log(emitter, 'thinking', 'Loading AGENT.md knowledge index...');
42+
const agentMd = await loadAgentMd();
43+
if (agentMd) {
44+
log(emitter, 'thinking', `AGENT.md loaded (${agentMd.length} chars)`);
45+
} else {
46+
log(emitter, 'thinking', 'AGENT.md not found — will operate without knowledge index');
47+
}
48+
2849
log(emitter, 'thinking', 'Loading skills...');
2950
const skills = await loadSkills();
3051
log(emitter, 'thinking', `Loaded ${skills.length} skills: ${skills.map(s => s.name).join(', ')}`);
@@ -47,6 +68,24 @@ CRITICAL RULES:
4768
4869
`;
4970

71+
// AGENT.md is the first thing the agent sees — the master knowledge index
72+
if (agentMd) {
73+
prompt += `<agent_knowledge_index>
74+
The following is your master knowledge index (AGENT.md). READ THIS FIRST before every request. It tells you exactly what skills, memory, infrastructure data, incident history, and learned patterns are available and where to find them. Use this to plan your approach before taking action.
75+
76+
${agentMd}
77+
</agent_knowledge_index>
78+
79+
IMPORTANT: Before responding to any user request, consult the knowledge index above to:
80+
1. Identify which skills are relevant to the issue
81+
2. Check if similar incidents have been investigated before
82+
3. Look up known infrastructure topology and configurations
83+
4. Review learned patterns for proven investigation sequences
84+
Then form an action plan and execute it.
85+
86+
`;}
87+
88+
5089
if (skills.length > 0) {
5190
prompt += '<skills>\n';
5291
for (const skill of skills) {
@@ -297,6 +336,16 @@ export async function runAgent({ message, images, conversationId, emitter }) {
297336
}
298337
}
299338

339+
// Regenerate AGENT.md if any memory was written
340+
if (memoryWrites.length > 0) {
341+
try {
342+
await regenerateAgentMd();
343+
log(emitter, 'thinking', 'AGENT.md updated with new knowledge');
344+
} catch (err) {
345+
console.error('[agent-md] Regeneration failed:', err.message);
346+
}
347+
}
348+
300349
let finalResponse = stripToolCalls(assistantText);
301350
finalResponse = stripMemoryWrites(finalResponse);
302351

@@ -493,6 +542,16 @@ ${getToolInstructions()}`;
493542
log(emitter, 'thinking', `Fed ${results.length} tool result(s) back to model, looping...`);
494543
}
495544

545+
// Regenerate AGENT.md after sync
546+
if (filesWritten.length > 0) {
547+
try {
548+
await regenerateAgentMd();
549+
log(emitter, 'thinking', 'AGENT.md updated after sync');
550+
} catch (err) {
551+
console.error('[agent-md] Regeneration failed:', err.message);
552+
}
553+
}
554+
496555
log(emitter, 'done', { message: `Sync complete: ${iterations} iterations, ${filesWritten.length} files written`, filesWritten });
497556
return { filesWritten };
498557
}
@@ -502,3 +561,6 @@ function extractField(content, fieldName) {
502561
const match = content.match(regex);
503562
return match ? match[1].trim() : null;
504563
}
564+
565+
// Exported for testing
566+
export { parseToolCalls, parseMemoryWrites, stripToolCalls, stripMemoryWrites, extractField, tryParseJson, isValidJson };

droid-agent/app/learner.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getUnanalyzedConversations, saveLearnerRun, getLastLearnerRun } from '.
22
import { readMemory, writeMemory } from './memory.js';
33
import { loadSkills } from './skills.js';
44
import { createCompletion } from './provider.js';
5+
import { regenerateAgentMd } from './agent-md.js';
56

67
const LEARNER_ENABLED = (process.env.LEARNER_ENABLED || 'true').toLowerCase() === 'true';
78
const LEARNER_INTERVAL_MS = parseInt(process.env.LEARNER_INTERVAL_MS) || 3600000; // 1 hour
@@ -127,6 +128,16 @@ Your job is to produce FOUR sections separated by ===SPLIT===:
127128
filesWritten
128129
});
129130

131+
// Regenerate AGENT.md after learner writes
132+
if (filesWritten.length > 0) {
133+
try {
134+
await regenerateAgentMd();
135+
console.log('[learner] AGENT.md updated');
136+
} catch (err) {
137+
console.error('[agent-md] Regeneration failed:', err.message);
138+
}
139+
}
140+
130141
console.log(`[learner] Cycle complete: analyzed ${conversations.length} conversations, wrote ${filesWritten.length} files`);
131142
return { conversationsAnalyzed: conversations.length, filesWritten };
132143
} catch (err) {

0 commit comments

Comments
 (0)