diff --git a/.oxfmtrc.json b/.oxfmtrc.json index f1882ab..3f6caf3 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,7 +1,5 @@ { "$schema": "./node_modules/oxfmt/configuration_schema.json", "ignorePatterns": ["*.md"], - "options": { - "singleQuote": true - } + "singleQuote": true } diff --git a/package.json b/package.json index 9b90e4c..f2c8253 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,6 @@ "lint": "oxlint", "lint:fix": "oxlint --fix" }, - "devDependencies": { - "@types/bun": "latest", - "@types/mozilla__readability": "^0.4.2", - "oxfmt": "^0.40.0", - "oxlint": "^1.55.0", - "oxlint-tsgolint": "^0.16.0" - }, - "peerDependencies": { - "typescript": "^5" - }, "dependencies": { "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/google": "^3.0.43", @@ -37,5 +27,15 @@ "ollama-ai-provider": "^1.2.0", "picocolors": "^1.1.1", "zod": "^4.3.6" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/mozilla__readability": "^0.4.2", + "oxfmt": "^0.40.0", + "oxlint": "^1.55.0", + "oxlint-tsgolint": "^0.16.0" + }, + "peerDependencies": { + "typescript": "^5" } } diff --git a/src/agent/loop.ts b/src/agent/loop.ts index 5cf9c58..2fba274 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -57,7 +57,11 @@ export class AgentLoop { this._model = opts.model ?? opts.provider.defaultModel; this._maxIterations = opts.maxIterations ?? 40; - const execConfig = opts.execConfig ?? { timeout: 120, denyPatterns: [], restrictToWorkspace: false }; + const execConfig = opts.execConfig ?? { + timeout: 120, + denyPatterns: [], + restrictToWorkspace: false, + }; this._ctx = new ContextBuilder(opts.workspace); this._sessions = opts.sessionManager ?? new SessionManager(opts.workspace); @@ -94,7 +98,11 @@ export class AgentLoop { restrictToWorkspace?: boolean; }): void { const allowed = opts.restrictToWorkspace ? this._workspace : undefined; - const execConfig = opts.execConfig ?? { timeout: 120, denyPatterns: [], restrictToWorkspace: false }; + const execConfig = opts.execConfig ?? { + timeout: 120, + denyPatterns: [], + restrictToWorkspace: false, + }; this._tools.register(new ReadFileTool({ workspace: this._workspace, allowedDir: allowed })); this._tools.register(new WriteFileTool({ workspace: this._workspace, allowedDir: allowed })); @@ -110,9 +118,7 @@ export class AgentLoop { ); this._tools.register(new WebSearchTool({ apiKey: opts.braveApiKey, proxy: opts.webProxy })); this._tools.register(new WebFetchTool({ proxy: opts.webProxy })); - this._tools.register( - new MessageTool((msg) => this._bus.publishOutbound(msg)), - ); + this._tools.register(new MessageTool((msg) => this._bus.publishOutbound(msg))); this._tools.register(new SpawnTool(this._subagents)); if (opts.cronService) { this._tools.register(new CronTool(opts.cronService)); @@ -191,7 +197,12 @@ export class AgentLoop { if (response) { this._bus.publishOutbound(response); } else if (msg.channel === 'cli') { - this._bus.publishOutbound({ channel: msg.channel, chatId: msg.chatId, content: '', metadata: msg.metadata }); + this._bus.publishOutbound({ + channel: msg.channel, + chatId: msg.chatId, + content: '', + metadata: msg.metadata, + }); } } catch (err) { if ((err as Error).name === 'AbortError') { @@ -215,17 +226,32 @@ export class AgentLoop { ): Promise { // System messages (subagent results) routed as "system" channel if (msg.channel === 'system') { - const [channel, chatId] = msg.chatId.includes(':') ? msg.chatId.split(':', 2) as [string, string] : ['cli', msg.chatId]; + const [channel, chatId] = msg.chatId.includes(':') + ? (msg.chatId.split(':', 2) as [string, string]) + : ['cli', msg.chatId]; const key = `${channel}:${chatId}`; const session = this._sessions.getOrCreate(key); await this._consolidator.maybeConsolidateByTokens(session); this._setToolContext(channel, chatId); - const messages = this._ctx.buildMessages({ history: session.getHistory(0) as Array>, currentMessage: msg.content, channel, chatId }); - const { finalContent, allMessages } = await this._runAgentLoop(messages as ModelMessage[], signal); + const messages = this._ctx.buildMessages({ + history: session.getHistory(0) as Array>, + currentMessage: msg.content, + channel, + chatId, + }); + const { finalContent, allMessages } = await this._runAgentLoop( + messages as ModelMessage[], + signal, + ); this._saveTurn(session, allMessages, 1 + session.getHistory(0).length); this._sessions.save(session); await this._consolidator.maybeConsolidateByTokens(session); - return { channel, chatId, content: finalContent ?? 'Background task completed.', metadata: {} }; + return { + channel, + chatId, + content: finalContent ?? 'Background task completed.', + metadata: {}, + }; } const preview = msg.content.length > 80 ? `${msg.content.slice(0, 80)}...` : msg.content; @@ -238,15 +264,31 @@ export class AgentLoop { const cmd = msg.content.trim().toLowerCase(); if (cmd === '/new') { if (!(await this._consolidator.archiveUnconsolidated(session))) { - return { channel: msg.channel, chatId: msg.chatId, content: 'Memory archival failed, session not cleared. Please try again.', metadata: {} }; + return { + channel: msg.channel, + chatId: msg.chatId, + content: 'Memory archival failed, session not cleared. Please try again.', + metadata: {}, + }; } session.clear(); this._sessions.save(session); this._sessions.invalidate(session.key); - return { channel: msg.channel, chatId: msg.chatId, content: 'New session started.', metadata: {} }; + return { + channel: msg.channel, + chatId: msg.chatId, + content: 'New session started.', + metadata: {}, + }; } if (cmd === '/help') { - return { channel: msg.channel, chatId: msg.chatId, content: 'nanobot commands:\n/new — Start a new conversation\n/stop — Stop the current task\n/help — Show available commands', metadata: {} }; + return { + channel: msg.channel, + chatId: msg.chatId, + content: + 'nanobot commands:\n/new — Start a new conversation\n/stop — Stop the current task\n/help — Show available commands', + metadata: {}, + }; } await this._consolidator.maybeConsolidateByTokens(session); @@ -256,7 +298,12 @@ export class AgentLoop { if (msgTool instanceof MessageTool) msgTool.startTurn(); const history = session.getHistory(0) as Array>; - const initialMessages = this._ctx.buildMessages({ history, currentMessage: msg.content, channel: msg.channel, chatId: msg.chatId }); + const initialMessages = this._ctx.buildMessages({ + history, + currentMessage: msg.content, + channel: msg.channel, + chatId: msg.chatId, + }); const onProgress = async (content: string, opts?: { toolHint?: boolean }) => { this._bus.publishOutbound({ @@ -267,7 +314,11 @@ export class AgentLoop { }); }; - const { finalContent, allMessages } = await this._runAgentLoop(initialMessages as ModelMessage[], signal, onProgress); + const { finalContent, allMessages } = await this._runAgentLoop( + initialMessages as ModelMessage[], + signal, + onProgress, + ); this._saveTurn(session, allMessages, 1 + history.length); this._sessions.save(session); @@ -311,13 +362,18 @@ export class AgentLoop { if (response.toolCalls.length > 0) { if (onProgress) { if (response.content) await onProgress(response.content); - const hint = response.toolCalls.map((tc) => { - const firstVal = Object.values(tc.arguments)[0]; - const display = typeof firstVal === 'string' - ? (firstVal.length > 40 ? `"${firstVal.slice(0, 40)}…"` : `"${firstVal}"`) - : ''; - return `${tc.name}(${display})`; - }).join(', '); + const hint = response.toolCalls + .map((tc) => { + const firstVal = Object.values(tc.arguments)[0]; + const display = + typeof firstVal === 'string' + ? firstVal.length > 40 + ? `"${firstVal.slice(0, 40)}…"` + : `"${firstVal}"` + : ''; + return `${tc.name}(${display})`; + }) + .join(', '); await onProgress(hint, { toolHint: true }); } @@ -353,7 +409,11 @@ export class AgentLoop { if (role === 'assistant' && !content && !(entry['tool_calls'] as unknown[])?.length) continue; // Truncate large tool results - if (role === 'tool' && typeof content === 'string' && content.length > TOOL_RESULT_MAX_CHARS) { + if ( + role === 'tool' && + typeof content === 'string' && + content.length > TOOL_RESULT_MAX_CHARS + ) { entry['content'] = `${content.slice(0, TOOL_RESULT_MAX_CHARS)}\n... (truncated)`; } diff --git a/src/agent/memory.ts b/src/agent/memory.ts index ed22196..dafe9a1 100644 --- a/src/agent/memory.ts +++ b/src/agent/memory.ts @@ -94,7 +94,11 @@ export class MemoryStore { return mem ? `## Long-term Memory\n${mem}` : ''; } - async consolidate(messages: Array>, provider: LLMProvider, model: string): Promise { + async consolidate( + messages: Array>, + provider: LLMProvider, + model: string, + ): Promise { if (messages.length === 0) return true; const currentMemory = this.readLongTerm(); @@ -104,7 +108,8 @@ export class MemoryStore { .map((m) => { const ts = typeof m['timestamp'] === 'string' ? m['timestamp'].slice(0, 16) : '?'; const role = (typeof m['role'] === 'string' ? m['role'] : 'unknown').toUpperCase(); - const content = typeof m['content'] === 'string' ? m['content'] : JSON.stringify(m['content']); + const content = + typeof m['content'] === 'string' ? m['content'] : JSON.stringify(m['content']); return `[${ts}] ${role}: ${content}`; }) .join('\n'); @@ -140,8 +145,10 @@ ${formatted}`; return false; } - const entry = typeof tc.arguments['history_entry'] === 'string' ? tc.arguments['history_entry'] : null; - const update = typeof tc.arguments['memory_update'] === 'string' ? tc.arguments['memory_update'] : null; + const entry = + typeof tc.arguments['history_entry'] === 'string' ? tc.arguments['history_entry'] : null; + const update = + typeof tc.arguments['memory_update'] === 'string' ? tc.arguments['memory_update'] : null; if (entry) this.appendHistory(entry); if (update && update !== currentMemory) this.writeLongTerm(update); @@ -165,7 +172,12 @@ export class MemoryConsolidator { private _model: string; private _sessions: SessionManager; private _contextWindowTokens: number; - private _buildMessages: (opts: { history: Array>; currentMessage: string; channel?: string; chatId?: string }) => Array>; + private _buildMessages: (opts: { + history: Array>; + currentMessage: string; + channel?: string; + chatId?: string; + }) => Array>; private _getToolDefs: () => Array>; private _locks = new Map>(); @@ -175,7 +187,12 @@ export class MemoryConsolidator { model: string; sessions: SessionManager; contextWindowTokens: number; - buildMessages: (opts: { history: Array>; currentMessage: string; channel?: string; chatId?: string }) => Array>; + buildMessages: (opts: { + history: Array>; + currentMessage: string; + channel?: string; + chatId?: string; + }) => Array>; getToolDefs: () => Array>; }) { this._store = new MemoryStore(opts.workspace); @@ -195,15 +212,23 @@ export class MemoryConsolidator { // Chain promises per session key to serialize consolidation const prev = this._locks.get(key) ?? Promise.resolve(); const next = prev.then(fn); - this._locks.set(key, next.catch(() => {})); + this._locks.set( + key, + next.catch(() => {}), + ); await next; } async archiveUnconsolidated(session: Session): Promise { let ok = false; await this._withLock(session.key, async () => { - const snapshot = session.messages.slice(session.lastConsolidated) as Array>; - if (snapshot.length === 0) { ok = true; return; } + const snapshot = session.messages.slice(session.lastConsolidated) as Array< + Record + >; + if (snapshot.length === 0) { + ok = true; + return; + } ok = await this._store.consolidate(snapshot, this._provider, this._model); }); return ok; @@ -219,7 +244,8 @@ export class MemoryConsolidator { const history = session.getHistory(0) as Array>; const probe = this._buildMessages({ history, currentMessage: '[token-probe]' }); const toolTokens = estimateTokens(JSON.stringify(this._getToolDefs())); - const estimated = estimateMessagesTokens(probe as Array>) + toolTokens; + const estimated = + estimateMessagesTokens(probe as Array>) + toolTokens; if (estimated < this._contextWindowTokens) return; // fits — done @@ -227,10 +253,14 @@ export class MemoryConsolidator { const boundary = this._pickBoundary(session, Math.max(1, estimated - target)); if (boundary === null) return; - const chunk = session.messages.slice(session.lastConsolidated, boundary) as Array>; + const chunk = session.messages.slice(session.lastConsolidated, boundary) as Array< + Record + >; if (chunk.length === 0) return; - console.info(`[memory] Token consolidation round ${round}: ~${estimated} tokens, chunk=${chunk.length} msgs`); + console.info( + `[memory] Token consolidation round ${round}: ~${estimated} tokens, chunk=${chunk.length} msgs`, + ); if (!(await this._store.consolidate(chunk, this._provider, this._model))) return; session.lastConsolidated = boundary; diff --git a/src/agent/skills.ts b/src/agent/skills.ts index 8d2808c..3003761 100644 --- a/src/agent/skills.ts +++ b/src/agent/skills.ts @@ -138,7 +138,10 @@ export class SkillsLoader { const colon = line.indexOf(':'); if (colon < 0) continue; const key = line.slice(0, colon).trim(); - const val = line.slice(colon + 1).trim().replace(/^["']|["']$/g, ''); + const val = line + .slice(colon + 1) + .trim() + .replace(/^["']|["']$/g, ''); if (key === 'description') meta.description = val; if (key === 'always') meta.always = val === 'true'; if (key === 'metadata') meta.metadata = val; diff --git a/src/agent/tools/cron.ts b/src/agent/tools/cron.ts index 0111211..518e9bf 100644 --- a/src/agent/tools/cron.ts +++ b/src/agent/tools/cron.ts @@ -55,12 +55,16 @@ export class CronTool implements Tool { case 'enable': { const id = strArg(args, 'id'); if (!id) return 'Error: id is required for enable.'; - return this._service.enableJob(id, true) ? `Job ${id} enabled.` : `Error: job ${id} not found.`; + return this._service.enableJob(id, true) + ? `Job ${id} enabled.` + : `Error: job ${id} not found.`; } case 'disable': { const id = strArg(args, 'id'); if (!id) return 'Error: id is required for disable.'; - return this._service.enableJob(id, false) ? `Job ${id} disabled.` : `Error: job ${id} not found.`; + return this._service.enableJob(id, false) + ? `Job ${id} disabled.` + : `Error: job ${id} not found.`; } case 'run': { const id = strArg(args, 'id'); diff --git a/src/agent/tools/filesystem.ts b/src/agent/tools/filesystem.ts index d42c142..0051979 100644 --- a/src/agent/tools/filesystem.ts +++ b/src/agent/tools/filesystem.ts @@ -5,7 +5,16 @@ import type { Tool } from './base.ts'; const MAX_READ_CHARS = 128_000; const MAX_ENTRIES = 2000; -const IGNORED_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', '.next', 'build']); +const IGNORED_DIRS = new Set([ + '.git', + 'node_modules', + '__pycache__', + '.venv', + 'venv', + 'dist', + '.next', + 'build', +]); // --------------------------------------------------------------------------- // read_file @@ -55,7 +64,10 @@ export class ReadFileTool implements Tool { const slice = lines.slice(start, end); const numbered = slice.map((l, i) => `${start + i + 1}: ${l}`).join('\n'); - const truncated = numbered.length > MAX_READ_CHARS ? numbered.slice(0, MAX_READ_CHARS) + '\n... (truncated)' : numbered; + const truncated = + numbered.length > MAX_READ_CHARS + ? numbered.slice(0, MAX_READ_CHARS) + '\n... (truncated)' + : numbered; const totalLines = lines.length; const header = `File: ${absPath} (${totalLines} lines total)\n`; @@ -160,7 +172,7 @@ export class EditFileTool implements Tool { let updated: string; if (replaceAll) { updated = content.split(oldString).join(newString); - count = (content.split(oldString).length - 1); + count = content.split(oldString).length - 1; } else { const idx = content.indexOf(oldString); if (idx === -1) return `Error: oldString not found in ${absPath}.`; diff --git a/src/agent/tools/shell.ts b/src/agent/tools/shell.ts index 1ea9400..789c801 100644 --- a/src/agent/tools/shell.ts +++ b/src/agent/tools/shell.ts @@ -7,12 +7,7 @@ const DEFAULT_TIMEOUT_S = 120; const MAX_TIMEOUT_S = 600; const OUTPUT_MAX_CHARS = 32_000; -const DEFAULT_DENY_PATTERNS = [ - /rm\s+-rf\s+\/(?!\S)/, - /mkfs/, - /dd\s+if=/, - /:\(\)\s*\{.*\}/, -]; +const DEFAULT_DENY_PATTERNS = [/rm\s+-rf\s+\/(?!\S)/, /mkfs/, /dd\s+if=/, /:\(\)\s*\{.*\}/]; export class ExecTool implements Tool { readonly name = 'exec'; diff --git a/src/agent/tools/spawn.ts b/src/agent/tools/spawn.ts index 58aa65e..92d7500 100644 --- a/src/agent/tools/spawn.ts +++ b/src/agent/tools/spawn.ts @@ -7,7 +7,10 @@ export class SpawnTool implements Tool { readonly description = 'Spawn a background subagent to handle a long-running task autonomously. The subagent has access to filesystem, shell, and web tools. It will report its result back when done.'; readonly parameters = { - task: { type: 'string', description: 'Full description of the task for the subagent to complete.' }, + task: { + type: 'string', + description: 'Full description of the task for the subagent to complete.', + }, }; readonly required = ['task']; diff --git a/src/agent/tools/web.ts b/src/agent/tools/web.ts index e392a0b..326e85c 100644 --- a/src/agent/tools/web.ts +++ b/src/agent/tools/web.ts @@ -12,7 +12,8 @@ const MAX_CONTENT_CHARS = 50_000; export class WebSearchTool implements Tool { readonly name = 'web_search'; - readonly description = 'Search the web using Brave Search. Returns a list of results with titles, URLs, and snippets.'; + readonly description = + 'Search the web using Brave Search. Returns a list of results with titles, URLs, and snippets.'; readonly parameters = { query: { type: 'string', description: 'Search query.' }, count: { type: 'number', description: 'Number of results (default 10, max 20).' }, @@ -30,7 +31,8 @@ export class WebSearchTool implements Tool { async execute(args: Record): Promise { const query = strArg(args, 'query').trim(); if (!query) return 'Error: query is required.'; - if (!this._apiKey) return 'Error: BRAVE_API_KEY not configured (set tools.web.braveApiKey in config).'; + if (!this._apiKey) + return 'Error: BRAVE_API_KEY not configured (set tools.web.braveApiKey in config).'; const count = Math.min(Number(args['count'] ?? 10), 20); const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`; @@ -38,7 +40,7 @@ export class WebSearchTool implements Tool { try { const res = await fetchWithTimeout(url, { headers: { - 'Accept': 'application/json', + Accept: 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': this._apiKey, }, @@ -46,7 +48,9 @@ export class WebSearchTool implements Tool { if (!res.ok) return `Error: Brave Search API returned ${res.status}: ${await res.text()}`; - const data = (await res.json()) as { web?: { results?: Array<{ title: string; url: string; description: string }> } }; + const data = (await res.json()) as { + web?: { results?: Array<{ title: string; url: string; description: string }> }; + }; const results = data.web?.results ?? []; if (results.length === 0) return 'No results found.'; @@ -70,7 +74,11 @@ export class WebFetchTool implements Tool { 'Fetch a URL and return its content. HTML pages are extracted to readable text. Use mode="raw" for JSON/XML/plain text.'; readonly parameters = { url: { type: 'string', description: 'URL to fetch.' }, - mode: { type: 'string', enum: ['markdown', 'text', 'raw'], description: 'Output mode (default: text).' }, + mode: { + type: 'string', + enum: ['markdown', 'text', 'raw'], + description: 'Output mode (default: text).', + }, }; readonly required = ['url']; @@ -96,8 +104,14 @@ export class WebFetchTool implements Tool { const contentType = res.headers.get('content-type') ?? ''; const body = await res.text(); - if (mode === 'raw' || (!contentType.includes('text/html') && !body.trimStart().startsWith('<'))) { - const truncated = body.length > MAX_CONTENT_CHARS ? body.slice(0, MAX_CONTENT_CHARS) + '\n... (truncated)' : body; + if ( + mode === 'raw' || + (!contentType.includes('text/html') && !body.trimStart().startsWith('<')) + ) { + const truncated = + body.length > MAX_CONTENT_CHARS + ? body.slice(0, MAX_CONTENT_CHARS) + '\n... (truncated)' + : body; return truncated; } @@ -114,9 +128,10 @@ export class WebFetchTool implements Tool { const title = article?.title ?? ''; const textContent = article?.textContent ?? stripTags(body); const trimmed = textContent.replace(/\n{3,}/g, '\n\n').trim(); - const truncated = trimmed.length > MAX_CONTENT_CHARS - ? trimmed.slice(0, MAX_CONTENT_CHARS) + '\n... (truncated)' - : trimmed; + const truncated = + trimmed.length > MAX_CONTENT_CHARS + ? trimmed.slice(0, MAX_CONTENT_CHARS) + '\n... (truncated)' + : trimmed; return title ? `# ${title}\n\n${truncated}` : truncated; } catch (err) { @@ -136,7 +151,10 @@ function fetchWithTimeout(url: string, init: RequestInit = {}): Promise]*>/g, ' ').replace(/\s+/g, ' ').trim(); + return html + .replace(/<[^>]*>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); } /** Build a minimal pseudo-document that satisfies Readability's interface. */ @@ -166,7 +184,9 @@ function makePseudoDocument( createTreeWalker: () => ({ nextNode: () => null }), createRange: () => ({ selectNodeContents: () => {}, cloneContents: () => null }), // biome-ignore lint/suspicious/noExplicitAny: Readability duck-typing - get innerHTML() { return html; }, + get innerHTML() { + return html; + }, location: { href: url }, }; diff --git a/src/channels/manager.ts b/src/channels/manager.ts index 64fd0c9..bca7f84 100644 --- a/src/channels/manager.ts +++ b/src/channels/manager.ts @@ -60,7 +60,7 @@ export class ChannelManager { } const content = msg.content ?? ''; - const chatId = msg.metadata?.['channel_id'] as string | undefined ?? msg.chatId; + const chatId = (msg.metadata?.['channel_id'] as string | undefined) ?? msg.chatId; const rootId = msg.metadata?.['root_id'] as string | undefined; try { diff --git a/src/channels/mattermost.ts b/src/channels/mattermost.ts index 3fb909e..85380cd 100644 --- a/src/channels/mattermost.ts +++ b/src/channels/mattermost.ts @@ -165,7 +165,11 @@ export class MattermostChannel extends BaseChannel { } else { // Group channel if (!this._shouldRespondInGroup(post.message, this._cfg.groupPolicy)) return; - if (this._cfg.groupPolicy === 'allowlist' && !this.isAllowed(post.user_id, this._cfg.groupAllowFrom)) return; + if ( + this._cfg.groupPolicy === 'allowlist' && + !this.isAllowed(post.user_id, this._cfg.groupAllowFrom) + ) + return; if (!this.isAllowed(post.user_id, this._cfg.allowFrom)) return; } @@ -226,7 +230,7 @@ export class MattermostChannel extends BaseChannel { const res = await fetch(`${this._baseUrl}${path}`, { method, headers: { - 'Authorization': `Bearer ${this._cfg.token}`, + Authorization: `Bearer ${this._cfg.token}`, 'Content-Type': 'application/json', }, body: body !== undefined ? JSON.stringify(body) : undefined, diff --git a/src/config/types.ts b/src/config/types.ts index c138191..1f22a0f 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -87,7 +87,11 @@ export const WebToolConfigSchema = z.object({ export type WebToolConfig = z.infer; export const ToolsConfigSchema = z.object({ - exec: ExecToolConfigSchema.default(() => ({ timeout: 120, denyPatterns: [], restrictToWorkspace: false })), + exec: ExecToolConfigSchema.default(() => ({ + timeout: 120, + denyPatterns: [], + restrictToWorkspace: false, + })), web: WebToolConfigSchema.default(() => ({})), restrictToWorkspace: z.boolean().default(false), }); diff --git a/src/cron/service.ts b/src/cron/service.ts index 8805d74..e1772f0 100644 --- a/src/cron/service.ts +++ b/src/cron/service.ts @@ -125,7 +125,11 @@ export class CronService { if (delayMs === null) return; const nextRunAtMs = Date.now() + delayMs; - const updated: CronJob = { ...job, state: { ...job.state, nextRunAtMs }, updatedAtMs: Date.now() }; + const updated: CronJob = { + ...job, + state: { ...job.state, nextRunAtMs }, + updatedAtMs: Date.now(), + }; this._jobs.set(job.id, updated); this._save(); @@ -159,7 +163,12 @@ export class CronService { } catch (err) { const updated: CronJob = { ...job, - state: { ...job.state, lastRunAtMs: Date.now(), lastStatus: 'error', lastError: String(err) }, + state: { + ...job.state, + lastRunAtMs: Date.now(), + lastStatus: 'error', + lastError: String(err), + }, updatedAtMs: Date.now(), }; this._jobs.set(job.id, updated); diff --git a/src/cron/types.ts b/src/cron/types.ts index fdb9ca3..4c7a7b5 100644 --- a/src/cron/types.ts +++ b/src/cron/types.ts @@ -29,8 +29,17 @@ export const CronJobSchema = z.object({ name: z.string(), enabled: z.boolean().default(true), schedule: CronScheduleSchema, - payload: CronPayloadSchema.default(() => ({ kind: 'agent_turn' as const, message: '', deliver: false })), - state: CronJobStateSchema.default(() => ({ nextRunAtMs: null, lastRunAtMs: null, lastStatus: null, lastError: null })), + payload: CronPayloadSchema.default(() => ({ + kind: 'agent_turn' as const, + message: '', + deliver: false, + })), + state: CronJobStateSchema.default(() => ({ + nextRunAtMs: null, + lastRunAtMs: null, + lastStatus: null, + lastError: null, + })), createdAtMs: z.number().int().default(0), updatedAtMs: z.number().int().default(0), deleteAfterRun: z.boolean().default(false), diff --git a/src/heartbeat/service.ts b/src/heartbeat/service.ts index 2ea9d53..4d8248f 100644 --- a/src/heartbeat/service.ts +++ b/src/heartbeat/service.ts @@ -117,9 +117,11 @@ export class HeartbeatService { return; } - const action = typeof decision.arguments['action'] === 'string' ? decision.arguments['action'] : 'skip'; + const action = + typeof decision.arguments['action'] === 'string' ? decision.arguments['action'] : 'skip'; if (action !== 'run') { - const reason = typeof decision.arguments['reason'] === 'string' ? decision.arguments['reason'] : ''; + const reason = + typeof decision.arguments['reason'] === 'string' ? decision.arguments['reason'] : ''; console.debug(`[heartbeat] Decision: skip (${reason})`); return; } diff --git a/src/provider/index.ts b/src/provider/index.ts index 0c4f431..70a6dee 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -70,7 +70,12 @@ export class LLMProvider { private _maxTokens: number; private _temperature: number; - constructor(providers: ProvidersConfig, defaultModel: string, maxTokens = 4096, temperature = 0.7) { + constructor( + providers: ProvidersConfig, + defaultModel: string, + maxTokens = 4096, + temperature = 0.7, + ) { this._providers = providers; this._defaultModel = defaultModel; this._maxTokens = maxTokens; @@ -106,7 +111,9 @@ export class LLMProvider { case 'ollama': { const cfg = this._providers.ollama; // ollama-ai-provider returns LanguageModelV1; cast to LanguageModel (compatible at runtime) - return createOllama({ baseURL: cfg?.apiBase ?? 'http://localhost:11434/api' })(remainder) as unknown as LanguageModel; + return createOllama({ baseURL: cfg?.apiBase ?? 'http://localhost:11434/api' })( + remainder, + ) as unknown as LanguageModel; } default: { // No recognized prefix — fall through to openai-compatible @@ -116,7 +123,9 @@ export class LLMProvider { } } - async chat(opts: ChatOptions): Promise<{ response: LLMResponse; responseMessages: ModelMessage[] }> { + async chat( + opts: ChatOptions, + ): Promise<{ response: LLMResponse; responseMessages: ModelMessage[] }> { const model = this._resolveModel(opts.model ?? this._defaultModel); const maxTokens = opts.maxTokens ?? this._maxTokens; const temperature = opts.temperature ?? this._temperature; @@ -142,7 +151,12 @@ export class LLMProvider { messages: opts.messages as ModelMessage[], // biome-ignore lint/suspicious/noExplicitAny: AI SDK tools type is complex tools: aiTools as any, - toolChoice: opts.toolChoice === 'required' ? 'required' : opts.toolChoice === 'none' ? 'none' : 'auto', + toolChoice: + opts.toolChoice === 'required' + ? 'required' + : opts.toolChoice === 'none' + ? 'none' + : 'auto', maxOutputTokens: maxTokens, temperature, stopWhen: stepCountIs(1), @@ -182,7 +196,9 @@ export class LLMProvider { } } - async chatWithRetry(opts: ChatOptions): Promise<{ response: LLMResponse; responseMessages: ModelMessage[] }> { + async chatWithRetry( + opts: ChatOptions, + ): Promise<{ response: LLMResponse; responseMessages: ModelMessage[] }> { for (const delay of RETRY_DELAYS_MS) { const result = await this.chat(opts); if (result.response.finishReason !== 'error') return result; @@ -207,7 +223,11 @@ export function makeProvider( } /** Build a tool-result message to append after executing a tool call. */ -export function toolResultMessage(toolCallId: string, toolName: string, result: string): ModelMessage { +export function toolResultMessage( + toolCallId: string, + toolName: string, + result: string, +): ModelMessage { return { role: 'tool', content: [ diff --git a/src/session/manager.ts b/src/session/manager.ts index 6369f4a..28842d2 100644 --- a/src/session/manager.ts +++ b/src/session/manager.ts @@ -126,10 +126,7 @@ export class SessionManager { save(session: Session): void { session.updatedAt = new Date().toISOString(); - const lines = [ - JSON.stringify(session.meta), - ...session.messages.map((m) => JSON.stringify(m)), - ]; + const lines = [JSON.stringify(session.meta), ...session.messages.map((m) => JSON.stringify(m))]; writeFileSync(this._filePath(session.key), lines.join('\n') + '\n', 'utf8'); }