Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Tests

on:
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: droid-agent/app/package-lock.json

- name: Install dependencies
working-directory: droid-agent/app
run: npm ci

- name: Run tests
working-directory: droid-agent/app
run: npm test
43 changes: 43 additions & 0 deletions droid-agent/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# AGENT.md — Droid Agent Knowledge Index

> 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.

---

## Skills (7 files)

Domain knowledge and runbooks the agent uses during investigations.

| File | Size | First line |
|------|------|------------|
| `skills/docker.md` | 3706b | ```bash |
| `skills/error-rate.md` | 2591b | When error rates spike, work through this in order: |
| `skills/general-debugging.md` | 5554b | When something breaks, answer these questions in order: |
| `skills/infra-sync.md` | 2157b | Discover everything about the developer's local and cloud infrastructure. Write |
| `skills/kubernetes-incidents.md` | 5085b | Run these immediately when something is wrong in a k8s cluster: |
| `skills/kubernetes.md` | 4165b | ```bash |
| `skills/performance.md` | 3781b | When latency spikes or performance degrades: |

## Memory

### context.md
<!-- Edit this file with permanent facts about your infrastructure -->

### README.md
Persistent knowledge the agent reads on every message. Survives container restar

---

## How to use this index

- **Before investigating**: Check `infra/summary.md` and `context.md` for known infrastructure topology
- **For k8s issues**: Read `skills/kubernetes-incidents.md` + `infra/kubernetes.md`
- **For Docker issues**: Read `skills/docker.md` + `infra/docker.md`
- **For performance**: Read `skills/performance.md`
- **For error spikes**: Read `skills/error-rate.md`
- **For past incidents**: Check `incidents/` for similar issues and `learned/patterns.md` for known investigation flows
- **For general triage**: Read `skills/general-debugging.md`

---

*Last updated: 2026-03-18T12:24:15.792Z*
92 changes: 92 additions & 0 deletions droid-agent/app/agent-md.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { readFile, writeFile } from 'fs/promises';
import { readAllMemory } from './memory.js';
import { loadSkills } from './skills.js';

const AGENT_MD_PATH = '/app/AGENT.md';

export async function regenerateAgentMd() {
const skills = await loadSkills();
const memoryFiles = await readAllMemory();

// Group memory files by directory
const groups = {};
for (const f of memoryFiles) {
const slashIdx = f.path.indexOf('/');
const dir = slashIdx !== -1 ? f.path.substring(0, slashIdx) : '/';
if (!groups[dir]) groups[dir] = [];
groups[dir].push(f);
}

let md = `# AGENT.md — Droid Agent Knowledge Index

> 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.

---

## Skills (${skills.length} files)

Domain knowledge and runbooks the agent uses during investigations.

| File | Size | First line |
|------|------|------------|
`;

for (const skill of skills) {
const firstLine = (skill.content.split('\n').find(l => l.trim() && !l.startsWith('#')) || '').substring(0, 80);
md += `| \`skills/${skill.name}.md\` | ${skill.size}b | ${firstLine} |\n`;
}

md += `\n## Memory\n\n`;

// Root files
if (groups['/']) {
for (const f of groups['/']) {
const firstLine = (f.content.split('\n').find(l => l.trim() && !l.startsWith('#')) || '').substring(0, 80);
md += `### ${f.path}\n${firstLine}\n\n`;
}
}

// Directory groups
const dirOrder = Object.keys(groups).filter(d => d !== '/').sort();
for (const dir of dirOrder) {
const files = groups[dir];
md += `### ${dir}/ (${files.length} files)\n\n`;
md += `| File | Size | Summary |\n|------|------|---------|\n`;
for (const f of files) {
const fileName = f.path.substring(dir.length + 1);
// Extract first meaningful line or heading
const lines = f.content.split('\n');
let summary = '';
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('>') && !trimmed.startsWith('---') && !trimmed.startsWith('|')) {
summary = trimmed.substring(0, 100);
break;
}
}
md += `| \`${dir}/${fileName}\` | ${f.size}b | ${summary} |\n`;
}
md += `\n`;
}

md += `---

## How to use this index

- **Before investigating**: Check \`infra/summary.md\` and \`context.md\` for known infrastructure topology
- **For k8s issues**: Read \`skills/kubernetes-incidents.md\` + \`infra/kubernetes.md\`
- **For Docker issues**: Read \`skills/docker.md\` + \`infra/docker.md\`
- **For performance**: Read \`skills/performance.md\`
- **For error spikes**: Read \`skills/error-rate.md\`
- **For past incidents**: Check \`incidents/\` for similar issues and \`learned/patterns.md\` for known investigation flows
- **For general triage**: Read \`skills/general-debugging.md\`

---

*Last updated: ${new Date().toISOString()}*
`;

await writeFile(AGENT_MD_PATH, md, 'utf-8');
console.log(`[agent-md] Regenerated AGENT.md (${md.length} chars, ${skills.length} skills, ${memoryFiles.length} memory files)`);
return md;
}
62 changes: 62 additions & 0 deletions droid-agent/app/agent.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { readFile } from 'fs/promises';
import { loadSkills, loadSkill } from './skills.js';
import { readAllMemory, readMemory, writeMemory } from './memory.js';
import { loadTools, loadMcpTools, executeTool } from './tools.js';
import { getConversation, saveConversation } from './redis.js';
import { logToolExecution, logIncident, upsertConversation, saveMessage, getRecentFeedback } from './db.js';
import { createCompletion, buildImageContent, getProviderName, getModelId } from './provider.js';
import { regenerateAgentMd } from './agent-md.js';

const AGENT_MD_PATH = '/app/AGENT.md';

async function loadAgentMd() {
try {
return await readFile(AGENT_MD_PATH, 'utf-8');
} catch {
return null;
}
}

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

async function buildSystemPrompt(emitter) {
// Load AGENT.md first — the master knowledge index
log(emitter, 'thinking', 'Loading AGENT.md knowledge index...');
const agentMd = await loadAgentMd();
if (agentMd) {
log(emitter, 'thinking', `AGENT.md loaded (${agentMd.length} chars)`);
} else {
log(emitter, 'thinking', 'AGENT.md not found — will operate without knowledge index');
}

log(emitter, 'thinking', 'Loading skills...');
const skills = await loadSkills();
log(emitter, 'thinking', `Loaded ${skills.length} skills: ${skills.map(s => s.name).join(', ')}`);
Expand All @@ -47,6 +68,24 @@ CRITICAL RULES:

`;

// AGENT.md is the first thing the agent sees — the master knowledge index
if (agentMd) {
prompt += `<agent_knowledge_index>
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.

${agentMd}
</agent_knowledge_index>

IMPORTANT: Before responding to any user request, consult the knowledge index above to:
1. Identify which skills are relevant to the issue
2. Check if similar incidents have been investigated before
3. Look up known infrastructure topology and configurations
4. Review learned patterns for proven investigation sequences
Then form an action plan and execute it.

`;}


if (skills.length > 0) {
prompt += '<skills>\n';
for (const skill of skills) {
Expand Down Expand Up @@ -297,6 +336,16 @@ export async function runAgent({ message, images, conversationId, emitter }) {
}
}

// Regenerate AGENT.md if any memory was written
if (memoryWrites.length > 0) {
try {
await regenerateAgentMd();
log(emitter, 'thinking', 'AGENT.md updated with new knowledge');
} catch (err) {
console.error('[agent-md] Regeneration failed:', err.message);
}
}

let finalResponse = stripToolCalls(assistantText);
finalResponse = stripMemoryWrites(finalResponse);

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

// Regenerate AGENT.md after sync
if (filesWritten.length > 0) {
try {
await regenerateAgentMd();
log(emitter, 'thinking', 'AGENT.md updated after sync');
} catch (err) {
console.error('[agent-md] Regeneration failed:', err.message);
}
}

log(emitter, 'done', { message: `Sync complete: ${iterations} iterations, ${filesWritten.length} files written`, filesWritten });
return { filesWritten };
}
Expand All @@ -502,3 +561,6 @@ function extractField(content, fieldName) {
const match = content.match(regex);
return match ? match[1].trim() : null;
}

// Exported for testing
export { parseToolCalls, parseMemoryWrites, stripToolCalls, stripMemoryWrites, extractField, tryParseJson, isValidJson };
11 changes: 11 additions & 0 deletions droid-agent/app/learner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getUnanalyzedConversations, saveLearnerRun, getLastLearnerRun } from '.
import { readMemory, writeMemory } from './memory.js';
import { loadSkills } from './skills.js';
import { createCompletion } from './provider.js';
import { regenerateAgentMd } from './agent-md.js';

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

// Regenerate AGENT.md after learner writes
if (filesWritten.length > 0) {
try {
await regenerateAgentMd();
console.log('[learner] AGENT.md updated');
} catch (err) {
console.error('[agent-md] Regeneration failed:', err.message);
}
}

console.log(`[learner] Cycle complete: analyzed ${conversations.length} conversations, wrote ${filesWritten.length} files`);
return { conversationsAnalyzed: conversations.length, filesWritten };
} catch (err) {
Expand Down
Loading
Loading