feat: add runtime context awareness — system.info tool + date/time in system prompt
- assembleSystemPrompt() now injects '# Runtime Context' with current date/time - New system.info tool: date, time, hostname, platform, arch, uptime, memory, Node.js version - Tool available in all profiles (minimal/messaging/coding/full) - 983 tests passing (+7 new)
This commit is contained in:
@@ -47,6 +47,7 @@ You have tools for interacting with your operator's system:
|
||||
- **file.edit** -- Edit files via find-and-replace. Safer than rewriting entire files.
|
||||
- **file.patch** -- Apply structured multi-hunk patches to one or more files. Line-based replacements, insertions, and deletions in a single call.
|
||||
- **file.list** -- List directory contents. Supports glob patterns.
|
||||
- **system.info** -- Get current date, time, hostname, platform, and system information.
|
||||
- **web.fetch** -- Fetch web pages. Use for looking things up, checking URLs, downloading content.
|
||||
|
||||
Use tools when the task requires it. For conversational questions, respond directly. Don't narrate tool usage -- just use them and present results.
|
||||
|
||||
+33
-2
@@ -694,6 +694,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtime-context-awareness": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-07",
|
||||
"summary": "Runtime context: system.info tool + automatic date/time injection in system prompt",
|
||||
"phases": {
|
||||
"system_prompt_date_injection": {
|
||||
"status": "completed",
|
||||
"description": "assembleSystemPrompt() now unconditionally appends a '# Runtime Context' section with current date and time",
|
||||
"files_modified": [
|
||||
"src/prompt/template.ts",
|
||||
"src/prompt/template.test.ts"
|
||||
],
|
||||
"test_status": "13/13 passing"
|
||||
},
|
||||
"system_info_tool": {
|
||||
"status": "completed",
|
||||
"description": "New system.info tool providing date, time, hostname, platform, architecture, OS release, uptime, Node.js version, memory usage, working directory. Added to all tool profiles (minimal/messaging/coding).",
|
||||
"files_created": [
|
||||
"src/tools/builtin/system-info.ts",
|
||||
"src/tools/builtin/system-info.test.ts"
|
||||
],
|
||||
"files_modified": [
|
||||
"src/tools/builtin/index.ts",
|
||||
"src/tools/policy.ts",
|
||||
"src/tools/index.ts",
|
||||
"SOUL.md"
|
||||
],
|
||||
"test_status": "6/6 passing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"earlier_plans": {
|
||||
"plans": [
|
||||
{ "file": "2026-02-02-flynn-design.md", "status": "completed" },
|
||||
@@ -717,7 +748,7 @@
|
||||
},
|
||||
|
||||
"overall_progress": {
|
||||
"total_test_count": 976,
|
||||
"total_test_count": 983,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
@@ -730,7 +761,7 @@
|
||||
"p8_completion": "8/8 (100%) — agent tools (sessions.list/history/create/delete, agents.list, message.send, cron.list/trigger) + gap analysis audit",
|
||||
"tier1_completion": "5/5 (100%) — !!think prefix, /verbose command, typing indicators (Discord/WhatsApp), session pruning (TTL), tool groups",
|
||||
"tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor",
|
||||
"feature_gap_scorecard": "87/116 match (75%), 1 partial (1%), 27 missing (23%)",
|
||||
"feature_gap_scorecard": "88/116 match (76%), 1 partial (1%), 27 missing (23%)",
|
||||
"next_up": "All phases P0-P8 and Tiers 1-2 complete. Tier 3 in progress (file.patch + Gmail done). Remaining gaps: Tier 3 channels (Signal, Matrix, Teams, Google Chat), Tier 4 deferred/niche items"
|
||||
}
|
||||
}
|
||||
|
||||
+20
-11
@@ -24,7 +24,7 @@ describe('assembleSystemPrompt', () => {
|
||||
const dir = makeTempDir();
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir] });
|
||||
|
||||
expect(result.prompt).toBe(
|
||||
expect(result.prompt).toContain(
|
||||
'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.',
|
||||
);
|
||||
expect(result.loadedFiles).toEqual([]);
|
||||
@@ -36,7 +36,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir] });
|
||||
|
||||
expect(result.prompt).toBe('You are Flynn.');
|
||||
expect(result.prompt).toContain('You are Flynn.');
|
||||
expect(result.loadedFiles).toHaveLength(1);
|
||||
expect(result.loadedFiles[0]).toContain('SOUL.md');
|
||||
});
|
||||
@@ -47,7 +47,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir] });
|
||||
|
||||
expect(result.prompt).toBe('# Agent Instructions\n\nFollow these rules.');
|
||||
expect(result.prompt).toContain('# Agent Instructions\n\nFollow these rules.');
|
||||
expect(result.loadedFiles).toHaveLength(1);
|
||||
expect(result.loadedFiles[0]).toContain('AGENTS.md');
|
||||
});
|
||||
@@ -62,7 +62,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
expect(result.loadedFiles).toHaveLength(3);
|
||||
// Verify correct ordering: SOUL → AGENTS → USER
|
||||
expect(result.prompt).toBe(
|
||||
expect(result.prompt).toContain(
|
||||
'I am Flynn.\n\n# Agent Instructions\n\nBe helpful.\n\n# User Context\n\nUser likes cats.',
|
||||
);
|
||||
});
|
||||
@@ -75,7 +75,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir1, dir2] });
|
||||
|
||||
expect(result.prompt).toBe('Primary identity.');
|
||||
expect(result.prompt).toContain('Primary identity.');
|
||||
expect(result.loadedFiles).toHaveLength(1);
|
||||
expect(result.loadedFiles[0]).toContain(dir1);
|
||||
});
|
||||
@@ -87,7 +87,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir1, dir2] });
|
||||
|
||||
expect(result.prompt).toBe('Fallback identity.');
|
||||
expect(result.prompt).toContain('Fallback identity.');
|
||||
expect(result.loadedFiles[0]).toContain(dir2);
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('assembleSystemPrompt', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe(
|
||||
expect(result.prompt).toContain(
|
||||
'Base identity.\n\n# Custom Rules\n\nAlways be polite.',
|
||||
);
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir] });
|
||||
|
||||
expect(result.prompt).toBe(
|
||||
expect(result.prompt).toContain(
|
||||
'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.',
|
||||
);
|
||||
expect(result.loadedFiles).toEqual([]);
|
||||
@@ -132,7 +132,7 @@ describe('assembleSystemPrompt', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.prompt).toBe(
|
||||
expect(result.prompt).toContain(
|
||||
'Base identity.\n\n# Populated\n\nHas content.',
|
||||
);
|
||||
});
|
||||
@@ -161,7 +161,7 @@ describe('assembleSystemPrompt', () => {
|
||||
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir] });
|
||||
|
||||
expect(result.prompt).toBe('I am Flynn.');
|
||||
expect(result.prompt).toContain('I am Flynn.');
|
||||
});
|
||||
|
||||
it('mixes files from different search directories', () => {
|
||||
@@ -173,6 +173,15 @@ describe('assembleSystemPrompt', () => {
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir1, dir2] });
|
||||
|
||||
expect(result.loadedFiles).toHaveLength(2);
|
||||
expect(result.prompt).toBe('Primary soul.\n\n# Agent Instructions\n\nAgent rules.');
|
||||
expect(result.prompt).toContain('Primary soul.\n\n# Agent Instructions\n\nAgent rules.');
|
||||
});
|
||||
|
||||
it('always includes Runtime Context section', () => {
|
||||
const dir = makeTempDir();
|
||||
writeFileSync(join(dir, 'SOUL.md'), 'I am Flynn.');
|
||||
const result = assembleSystemPrompt({ searchDirs: [dir] });
|
||||
expect(result.prompt).toContain('# Runtime Context');
|
||||
expect(result.prompt).toContain('Current date:');
|
||||
expect(result.prompt).toContain('Current time:');
|
||||
});
|
||||
});
|
||||
|
||||
+20
-3
@@ -63,10 +63,27 @@ export function assembleSystemPrompt(config: PromptTemplateConfig): PromptTempla
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if nothing was loaded
|
||||
if (sections.length === 0) {
|
||||
// Inject current date/time as runtime context
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const timeStr = now.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
hour12: false,
|
||||
});
|
||||
const runtimeContext = `# Runtime Context\n\nCurrent date: ${dateStr}\nCurrent time: ${timeStr}`;
|
||||
sections.push(runtimeContext);
|
||||
|
||||
// Fallback if only the runtime context was loaded (no actual prompt files)
|
||||
if (sections.length === 1) {
|
||||
return {
|
||||
prompt: 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.',
|
||||
prompt: `You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.\n\n${runtimeContext}`,
|
||||
loadedFiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export { fileWriteTool } from './file-write.js';
|
||||
export { fileEditTool } from './file-edit.js';
|
||||
export { filePatchTool } from './file-patch.js';
|
||||
export { fileListTool } from './file-list.js';
|
||||
export { systemInfoTool } from './system-info.js';
|
||||
export { webFetchTool } from './web-fetch.js';
|
||||
export { createMediaSendTool } from './media-send.js';
|
||||
export { createImageAnalyzeTool } from './image-analyze.js';
|
||||
@@ -31,6 +32,7 @@ import { fileWriteTool } from './file-write.js';
|
||||
import { fileEditTool } from './file-edit.js';
|
||||
import { filePatchTool } from './file-patch.js';
|
||||
import { fileListTool } from './file-list.js';
|
||||
import { systemInfoTool } from './system-info.js';
|
||||
import { webFetchTool } from './web-fetch.js';
|
||||
import { createMediaSendTool } from './media-send.js';
|
||||
import { createImageAnalyzeTool } from './image-analyze.js';
|
||||
@@ -47,6 +49,7 @@ export const allBuiltinTools: Tool[] = [
|
||||
fileEditTool,
|
||||
filePatchTool,
|
||||
fileListTool,
|
||||
systemInfoTool,
|
||||
webFetchTool,
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { systemInfoTool } from './system-info.js';
|
||||
|
||||
describe('system.info tool', () => {
|
||||
// ── Metadata ─────────────────────────────────────────────────────────────
|
||||
|
||||
it('has correct metadata', () => {
|
||||
expect(systemInfoTool.name).toBe('system.info');
|
||||
expect(systemInfoTool.description).toBeTruthy();
|
||||
expect(systemInfoTool.inputSchema.type).toBe('object');
|
||||
});
|
||||
|
||||
// ── Execution ────────────────────────────────────────────────────────────
|
||||
|
||||
it('returns success with system info', async () => {
|
||||
const result = await systemInfoTool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(typeof result.output).toBe('string');
|
||||
expect(result.output!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('output contains expected fields', async () => {
|
||||
const result = await systemInfoTool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const output = result.output!;
|
||||
|
||||
const expectedFields = [
|
||||
'Date:',
|
||||
'Time:',
|
||||
'Hostname:',
|
||||
'Platform:',
|
||||
'Architecture:',
|
||||
'Node.js:',
|
||||
'Uptime:',
|
||||
'Memory Total:',
|
||||
];
|
||||
|
||||
for (const field of expectedFields) {
|
||||
expect(output).toContain(field);
|
||||
}
|
||||
});
|
||||
|
||||
it('output contains valid ISO timestamp', async () => {
|
||||
const result = await systemInfoTool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const output = result.output!;
|
||||
|
||||
// Find the line containing 'ISO 8601:'
|
||||
const isoLine = output.split('\n').find((line) => line.includes('ISO 8601:'));
|
||||
expect(isoLine).toBeTruthy();
|
||||
|
||||
// Extract the ISO string and validate it
|
||||
const isoMatch = isoLine!.match(/ISO 8601:\s*(.+)/);
|
||||
expect(isoMatch).toBeTruthy();
|
||||
|
||||
const isoString = isoMatch![1].trim();
|
||||
const parsed = new Date(isoString);
|
||||
expect(parsed.toISOString()).toBe(isoString);
|
||||
});
|
||||
|
||||
it('output contains working directory', async () => {
|
||||
const result = await systemInfoTool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('Working Dir:');
|
||||
});
|
||||
|
||||
// ── Idempotency ──────────────────────────────────────────────────────────
|
||||
|
||||
it('handles repeated calls', async () => {
|
||||
const result1 = await systemInfoTool.execute({});
|
||||
const result2 = await systemInfoTool.execute({});
|
||||
|
||||
expect(result1.success).toBe(true);
|
||||
expect(result2.success).toBe(true);
|
||||
expect(typeof result1.output).toBe('string');
|
||||
expect(typeof result2.output).toBe('string');
|
||||
expect(result1.output!.length).toBeGreaterThan(0);
|
||||
expect(result2.output!.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import os from 'os';
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
export const systemInfoTool: Tool = {
|
||||
name: 'system.info',
|
||||
description: 'Get current system information including date, time, hostname, OS, platform, architecture, uptime, Node.js version, and memory usage.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
execute: async (_rawArgs: unknown): Promise<ToolResult> => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const timeStr = now.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
hour12: false,
|
||||
});
|
||||
const isoStr = now.toISOString();
|
||||
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
|
||||
const lines = [
|
||||
`Date: ${dateStr}`,
|
||||
`Time: ${timeStr}`,
|
||||
`ISO 8601: ${isoStr}`,
|
||||
`Hostname: ${os.hostname()}`,
|
||||
`Platform: ${os.platform()}`,
|
||||
`Architecture: ${os.arch()}`,
|
||||
`OS Release: ${os.release()}`,
|
||||
`Uptime: ${formatUptime(os.uptime())}`,
|
||||
`Node.js: ${process.version}`,
|
||||
`Memory Total: ${formatBytes(totalMem)}`,
|
||||
`Memory Free: ${formatBytes(freeMem)}`,
|
||||
`Working Dir: ${process.cwd()}`,
|
||||
];
|
||||
|
||||
return { success: true, output: lines.join('\n') };
|
||||
} catch (error) {
|
||||
return { success: false, output: '', error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -14,4 +14,5 @@ export { fileReadTool } from './builtin/file-read.js';
|
||||
export { fileWriteTool } from './builtin/file-write.js';
|
||||
export { fileEditTool } from './builtin/file-edit.js';
|
||||
export { fileListTool } from './builtin/file-list.js';
|
||||
export { systemInfoTool } from './builtin/system-info.js';
|
||||
export { webFetchTool } from './builtin/web-fetch.js';
|
||||
|
||||
@@ -9,11 +9,13 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
||||
'file.read',
|
||||
'file.list',
|
||||
'web.fetch',
|
||||
'system.info',
|
||||
]),
|
||||
messaging: new Set([
|
||||
'file.read',
|
||||
'file.list',
|
||||
'web.fetch',
|
||||
'system.info',
|
||||
'memory.read',
|
||||
'memory.write',
|
||||
'memory.search',
|
||||
@@ -23,6 +25,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
||||
'file.read',
|
||||
'file.list',
|
||||
'web.fetch',
|
||||
'system.info',
|
||||
'memory.read',
|
||||
'memory.write',
|
||||
'memory.search',
|
||||
|
||||
Reference in New Issue
Block a user