From 6386d0e285d77aaacfd9304df590e3f12c0a49ab Mon Sep 17 00:00:00 2001 From: Declan Jackson Date: Thu, 15 Jan 2026 13:39:25 +1100 Subject: [PATCH 1/2] fix url to repo --- docs/extending/full-customization.md | 4 ++-- docs/index.md | 2 +- mkdocs.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/extending/full-customization.md b/docs/extending/full-customization.md index 472ed7d..854e31c 100644 --- a/docs/extending/full-customization.md +++ b/docs/extending/full-customization.md @@ -22,7 +22,7 @@ For most use cases, you don't need full customization: ### 1. Clone the Repository ```bash -git clone https://github.com/ArtificialAnalysis/stirrup-js.git +git clone https://github.com/ArtificialAnalysis/stirrupJS.git cd stirrup-js ``` @@ -402,7 +402,7 @@ See `CONTRIBUTING.md` in the repository. Check out these projects using StirrupJS: -- [StirrupJS](https://github.com/ArtificialAnalysis/stirrup-js) - The main repository +- [StirrupJS](https://github.com/ArtificialAnalysis/stirrupJS) - The main repository - [Stirrup](https://github.com/ArtificialAnalysis/Stirrup) - Python version ## Best Practices diff --git a/docs/index.md b/docs/index.md index d53944b..5549426 100644 --- a/docs/index.md +++ b/docs/index.md @@ -164,7 +164,7 @@ For deep customization of the framework internals, you can clone and modify Stir ```bash # Clone the repository -git clone https://github.com/ArtificialAnalysis/stirrup-js.git +git clone https://github.com/ArtificialAnalysis/stirrupJS.git cd stirrup-js # Install dependencies diff --git a/mkdocs.yml b/mkdocs.yml index 119e66b..d714a60 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,8 +2,8 @@ site_name: StirrupJS Documentation site_url: https://stirrupjs.artificialanalysis.ai site_description: Lightweight TypeScript/JavaScript framework for building AI agents site_author: Artificial Analysis, Inc. -repo_url: https://github.com/ArtificialAnalysis/stirrup-js -repo_name: stirrup-js +repo_url: https://github.com/ArtificialAnalysis/stirrupJS +repo_name: stirrupJS theme: name: material From 5278504a639fa95a44ddffc8594d07634f388eb5 Mon Sep 17 00:00:00 2001 From: Declan Jackson Date: Thu, 15 Jan 2026 14:26:24 +1100 Subject: [PATCH 2/2] chore: format and update ci --- .github/workflows/ci.yaml | 73 ++++++--- src/clients/anthropic-client.ts | 9 +- src/clients/openai-client.ts | 11 +- src/clients/utils.ts | 11 +- src/clients/vercel-ai-gateway-client.ts | 9 +- src/content/processors.ts | 6 +- src/core/agent.ts | 169 +++++++------------- src/core/models.ts | 11 +- src/index.ts | 31 +++- src/skills/index.ts | 5 - src/skills/skills.ts | 5 - src/tools/code-exec/docker.ts | 26 +-- src/tools/code-exec/index.ts | 7 +- src/tools/code-exec/local.ts | 3 +- src/tools/finish.ts | 8 +- src/tools/index.ts | 12 +- src/tools/mcp/provider.ts | 23 +-- src/tools/user-input.ts | 40 ++--- src/utils/async-stack.ts | 4 +- src/utils/logging/structured-logger.ts | 203 ++++++++++++++---------- 20 files changed, 332 insertions(+), 334 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1decea9..910aa81 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,47 +9,70 @@ on: - main jobs: - lint: + format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: "Set up Python" - uses: actions/setup-python@v6 + - name: Set up Node.js + uses: actions/setup-node@v4 with: - python-version-file: ".python-version" + node-version: "20" + cache: "npm" - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true + - name: Install dependencies + run: npm ci - - name: Sync dependencies - run: uv sync --locked --all-extras --dev + - name: Run format check + run: npm run format:check - - name: Run ruff format check - run: uv run ruff format --check . + type-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 - - name: Run ruff check - run: uv run ruff check --output-format=github . + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" - type-check: + - name: Install dependencies + run: npm ci + + - name: Run type checking + run: npm run typecheck + + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: "Set up Python" - uses: actions/setup-python@v6 + - name: Set up Node.js + uses: actions/setup-node@v4 with: - python-version-file: ".python-version" + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test -- --run + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v6 + - name: Set up Node.js + uses: actions/setup-node@v4 with: - enable-cache: true + node-version: "20" + cache: "npm" - - name: Sync dependencies - run: uv sync --locked --all-extras --dev + - name: Install dependencies + run: npm ci - - name: Run type checking - run: uv run ty check --output-format github + - name: Build + run: npm run build diff --git a/src/clients/anthropic-client.ts b/src/clients/anthropic-client.ts index d801d72..6e7a266 100644 --- a/src/clients/anthropic-client.ts +++ b/src/clients/anthropic-client.ts @@ -4,14 +4,7 @@ import Anthropic from '@anthropic-ai/sdk'; import retry from 'async-retry'; -import type { - LLMClient, - ChatMessage, - AssistantMessage, - Tool, - ToolCall, - TokenUsage, -} from '../core/models.js'; +import type { LLMClient, ChatMessage, AssistantMessage, Tool, ToolCall, TokenUsage } from '../core/models.js'; import { ContextOverflowError } from '../core/models.js'; import { toAnthropicMessages, toAnthropicTools } from './utils.js'; import { MAX_RETRY_ATTEMPTS, RETRY_MIN_TIMEOUT, RETRY_MAX_TIMEOUT } from '../constants.js'; diff --git a/src/clients/openai-client.ts b/src/clients/openai-client.ts index b8c0f07..db9823e 100644 --- a/src/clients/openai-client.ts +++ b/src/clients/openai-client.ts @@ -5,14 +5,7 @@ import OpenAI from 'openai'; import retry from 'async-retry'; -import type { - LLMClient, - ChatMessage, - AssistantMessage, - Tool, - ToolCall, - TokenUsage, -} from '../core/models.js'; +import type { LLMClient, ChatMessage, AssistantMessage, Tool, ToolCall, TokenUsage } from '../core/models.js'; import { ContextOverflowError } from '../core/models.js'; import { toOpenAIMessages, toOpenAITools } from './utils.js'; import { MAX_RETRY_ATTEMPTS, RETRY_MIN_TIMEOUT, RETRY_MAX_TIMEOUT } from '../constants.js'; @@ -177,7 +170,7 @@ export class ChatCompletionsClient implements LLMClient { input: response.usage.prompt_tokens, output: response.usage.completion_tokens, reasoning: this.config.includeReasoningTokens - ? usageWithDetails.completion_tokens_details?.reasoning_tokens ?? 0 + ? (usageWithDetails.completion_tokens_details?.reasoning_tokens ?? 0) : 0, }; } diff --git a/src/clients/utils.ts b/src/clients/utils.ts index b87c888..6619cd2 100644 --- a/src/clients/utils.ts +++ b/src/clients/utils.ts @@ -252,8 +252,7 @@ export function toAnthropicMessages(messages: ChatMessage[]): { // Add text content if (message.content) { - const textContent = - typeof message.content === 'string' ? message.content : JSON.stringify(message.content); + const textContent = typeof message.content === 'string' ? message.content : JSON.stringify(message.content); if (textContent) { (result.content as unknown[]).push({ type: 'text', @@ -325,9 +324,7 @@ export function toOpenAITools(tools: Map): unknown[] { for (const [key, value] of Object.entries(shape)) { properties[key] = zodToJsonSchema(value); const isOptionalFn = (value as { isOptional?: unknown }).isOptional; - const isOptional = typeof isOptionalFn === 'function' - ? (isOptionalFn as () => boolean)() - : false; + const isOptional = typeof isOptionalFn === 'function' ? (isOptionalFn as () => boolean)() : false; if (!isOptional) required.push(key); } } @@ -372,9 +369,7 @@ export function toAnthropicTools(tools: Map): unknown[] { for (const [key, value] of Object.entries(shape)) { properties[key] = zodToJsonSchema(value); const isOptionalFn = (value as { isOptional?: unknown }).isOptional; - const isOptional = typeof isOptionalFn === 'function' - ? (isOptionalFn as () => boolean)() - : false; + const isOptional = typeof isOptionalFn === 'function' ? (isOptionalFn as () => boolean)() : false; if (!isOptional) required.push(key); } } diff --git a/src/clients/vercel-ai-gateway-client.ts b/src/clients/vercel-ai-gateway-client.ts index 8a1ea95..9e1faf1 100644 --- a/src/clients/vercel-ai-gateway-client.ts +++ b/src/clients/vercel-ai-gateway-client.ts @@ -7,14 +7,7 @@ import type { LanguageModel, ModelMessage, ToolSet } from 'ai'; import { generateText } from 'ai'; import retry from 'async-retry'; import { MAX_RETRY_ATTEMPTS, RETRY_MAX_TIMEOUT, RETRY_MIN_TIMEOUT } from '../constants.js'; -import type { - AssistantMessage, - ChatMessage, - LLMClient, - TokenUsage, - Tool, - ToolCall, -} from '../core/models.js'; +import type { AssistantMessage, ChatMessage, LLMClient, TokenUsage, Tool, ToolCall } from '../core/models.js'; import { ContextOverflowError } from '../core/models.js'; export interface VercelAIClientConfig { diff --git a/src/content/processors.ts b/src/content/processors.ts index 0ba7f73..9027da6 100644 --- a/src/content/processors.ts +++ b/src/content/processors.ts @@ -12,11 +12,7 @@ import { fileTypeFromBuffer } from 'file-type'; * @param maxPixels Maximum total pixels * @returns Adjusted [width, height] with even dimensions */ -export function calculateDownscaledDimensions( - width: number, - height: number, - maxPixels: number -): [number, number] { +export function calculateDownscaledDimensions(width: number, height: number, maxPixels: number): [number, number] { const currentPixels = width * height; if (currentPixels <= maxPixels) { diff --git a/src/core/agent.ts b/src/core/agent.ts index 917081a..24b6159 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -4,11 +4,7 @@ import { EventEmitter } from 'events'; import { z } from 'zod'; -import { - AGENT_MAX_TURNS, - CONTEXT_SUMMARIZATION_CUTOFF, - FINISH_TOOL_NAME, -} from '../constants.js'; +import { AGENT_MAX_TURNS, CONTEXT_SUMMARIZATION_CUTOFF, FINISH_TOOL_NAME } from '../constants.js'; import { BASE_SYSTEM_PROMPT, MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE, MESSAGE_SUMMARIZER_PROMPT } from '../prompts/index.js'; import { type CodeExecToolProvider } from '../tools/code-exec/base.js'; import { formatSkillsSection, loadSkillsMetadata } from '../skills/index.js'; @@ -27,11 +23,7 @@ import type { ToolResult, UserMessage, } from './models.js'; -import { - AgentValidationError, - TokenUsageMetadata, - aggregateMetadata, -} from './models.js'; +import { AgentValidationError, TokenUsageMetadata, aggregateMetadata } from './models.js'; import { createSessionState, getParentDepth, sessionContext, type SessionState } from './session.js'; import { SubAgentMetadata, SubAgentParamsSchema, type SubAgentParams } from './sub-agent.js'; @@ -151,22 +143,10 @@ export interface AgentRunResult { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export declare interface Agent { - on>>( - event: E, - listener: AgentEvents>[E] - ): this; - once>>( - event: E, - listener: AgentEvents>[E] - ): this; - emit>>( - event: E, - ...args: Parameters>[E]> - ): boolean; - off>>( - event: E, - listener: AgentEvents>[E] - ): this; + on>>(event: E, listener: AgentEvents>[E]): this; + once>>(event: E, listener: AgentEvents>[E]): this; + emit>>(event: E, ...args: Parameters>[E]>): boolean; + off>>(event: E, listener: AgentEvents>[E]): this; } /** @@ -223,9 +203,7 @@ export class Agent extends Ev // Validate agent name if (!/^[a-zA-Z0-9_-]{1,128}$/.test(name)) { - throw new AgentValidationError( - 'Agent name must be alphanumeric (with _ or -) and 1-128 characters long' - ); + throw new AgentValidationError('Agent name must be alphanumeric (with _ or -) and 1-128 characters long'); } this.client = client; @@ -295,27 +273,23 @@ export class Agent extends Ev signal?.throwIfAborted(); - const messages: ChatMessage[] = typeof initMessages === 'string' - ? [{ role: 'user', content: initMessages } as UserMessage] - : initMessages; + const messages: ChatMessage[] = + typeof initMessages === 'string' ? [{ role: 'user', content: initMessages } as UserMessage] : initMessages; - const systemPrompt = this.buildSystemPrompt(); - const allMessages: ChatMessage[] = [ - { role: 'system', content: systemPrompt } as SystemMessage, - ...messages, - ]; + const systemPrompt = this.buildSystemPrompt(); + const allMessages: ChatMessage[] = [{ role: 'system', content: systemPrompt } as SystemMessage, ...messages]; - const messageHistory: ChatMessage[][] = []; - let currentMessages = allMessages; - let currentGroup: ChatMessage[] = [...currentMessages]; + const messageHistory: ChatMessage[][] = []; + let currentMessages = allMessages; + let currentGroup: ChatMessage[] = [...currentMessages]; - const runMetadata: Record = { - token_usage: [], - }; + const runMetadata: Record = { + token_usage: [], + }; - for (const toolName of this.activeTools.keys()) { - runMetadata[toolName] = []; - } + for (const toolName of this.activeTools.keys()) { + runMetadata[toolName] = []; + } let finishParams: z.infer | undefined; for (let turn = 0; turn < this.maxTurns; turn++) { @@ -327,9 +301,10 @@ export class Agent extends Ev if (assistantMessage.content) { this.emit('message:assistant', { - content: typeof assistantMessage.content === 'string' - ? assistantMessage.content - : JSON.stringify(assistantMessage.content), + content: + typeof assistantMessage.content === 'string' + ? assistantMessage.content + : JSON.stringify(assistantMessage.content), toolCalls: assistantMessage.toolCalls, }); } @@ -337,9 +312,7 @@ export class Agent extends Ev for (const toolMsg of toolMessages) { this.emit('message:tool', { name: toolMsg.name || 'unknown', - content: typeof toolMsg.content === 'string' - ? toolMsg.content - : JSON.stringify(toolMsg.content), + content: typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content), success: !toolMsg.content?.toString().includes('Error'), }); } @@ -353,26 +326,26 @@ export class Agent extends Ev const lastTokenUsage = tokenUsageArray?.[tokenUsageArray.length - 1]; this.emit('turn:complete', { turn, tokenUsage: lastTokenUsage }); - if (assistantMessage.toolCalls) { - for (const toolCall of assistantMessage.toolCalls) { - if (toolCall.name === FINISH_TOOL_NAME && this.finishTool) { - try { - const params = this.finishTool.parameters - ? (this.finishTool.parameters.parse(this.parseToolCallArguments(toolCall.arguments)) as z.infer) - : undefined; - finishParams = params; - break; - } catch { - // Invalid finish params, continue + if (assistantMessage.toolCalls) { + for (const toolCall of assistantMessage.toolCalls) { + if (toolCall.name === FINISH_TOOL_NAME && this.finishTool) { + try { + const params = this.finishTool.parameters + ? (this.finishTool.parameters.parse(this.parseToolCallArguments(toolCall.arguments)) as z.infer) + : undefined; + finishParams = params; + break; + } catch { + // Invalid finish params, continue + } } } } - } - if (finishParams !== undefined) { - messageHistory.push(currentGroup); - break; - } + if (finishParams !== undefined) { + messageHistory.push(currentGroup); + break; + } if (assistantMessage.tokenUsage) { const totalTokens = assistantMessage.tokenUsage.input + assistantMessage.tokenUsage.output; @@ -454,15 +427,11 @@ export class Agent extends Ev signal?.throwIfAborted(); - const messages: ChatMessage[] = typeof initMessages === 'string' - ? [{ role: 'user', content: initMessages } as UserMessage] - : initMessages; + const messages: ChatMessage[] = + typeof initMessages === 'string' ? [{ role: 'user', content: initMessages } as UserMessage] : initMessages; const systemPrompt = this.buildSystemPrompt(); - const allMessages: ChatMessage[] = [ - { role: 'system', content: systemPrompt } as SystemMessage, - ...messages, - ]; + const allMessages: ChatMessage[] = [{ role: 'system', content: systemPrompt } as SystemMessage, ...messages]; const messageHistory: ChatMessage[][] = []; let currentMessages = allMessages; @@ -500,9 +469,7 @@ export class Agent extends Ev yield { type: 'tool:result', toolName: toolMsg.name || 'unknown', - result: typeof toolMsg.content === 'string' - ? toolMsg.content - : JSON.stringify(toolMsg.content), + result: typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content), success: !toolMsg.content?.toString().includes('Error'), timestamp: Date.now(), }; @@ -669,9 +636,7 @@ export class Agent extends Ev try { result = await tool.executor(params); - const contentStr = typeof result.content === 'string' - ? result.content - : JSON.stringify(result.content); + const contentStr = typeof result.content === 'string' ? result.content : JSON.stringify(result.content); this.emit('tool:complete', { name: toolCall.name, result: contentStr, @@ -725,10 +690,7 @@ export class Agent extends Ev const summaryText = typeof summary === 'string' ? summary : JSON.stringify(summary); const bridgeMessage = MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE(summaryText); - return [ - ...taskContext, - { role: 'user', content: bridgeMessage } as UserMessage, - ]; + return [...taskContext, { role: 'user', content: bridgeMessage } as UserMessage]; } /** @@ -740,7 +702,7 @@ export class Agent extends Ev // User interaction guidance based on whether user_input tool is available if (this.activeTools.has('user_input')) { prompt += - "\n\nYou have access to the user_input tool which allows you to ask the user questions when you need clarification or are uncertain about something."; + '\n\nYou have access to the user_input tool which allows you to ask the user questions when you need clarification or are uncertain about something.'; } else { prompt += '\n\nYou are not able to interact with the user during the task.'; } @@ -783,9 +745,7 @@ export class Agent extends Ev const codeExecProviders = this.tools.filter((t): t is CodeExecToolProvider => this.isCodeExecProvider(t)); if (codeExecProviders.length > 1) { - throw new Error( - `Agent can only have one CodeExecToolProvider, found ${codeExecProviders.length}` - ); + throw new Error(`Agent can only have one CodeExecToolProvider, found ${codeExecProviders.length}`); } for (const tool of codeExecProviders) { @@ -847,9 +807,11 @@ export class Agent extends Ev if (!spec) continue; if (this.isGlobPattern(spec)) { // Node.js 22+ supports fs.promises.glob - const globFn = (fs as unknown as { - glob?: (pattern: string, opts?: Record) => Promise; - }).glob; + const globFn = ( + fs as unknown as { + glob?: (pattern: string, opts?: Record) => Promise; + } + ).glob; if (!globFn) { throw new Error('Glob patterns in inputFiles require Node.js 22+ (fs.promises.glob not available)'); } @@ -919,11 +881,11 @@ export class Agent extends Ev // Safe access to potential paths property on finish params const finishParams = this.lastFinishParams; let paths: string[] = []; - + if ( - finishParams && - typeof finishParams === 'object' && - 'paths' in finishParams && + finishParams && + typeof finishParams === 'object' && + 'paths' in finishParams && Array.isArray((finishParams as Record).paths) ) { paths = (finishParams as Record).paths as string[]; @@ -942,27 +904,18 @@ export class Agent extends Ev ); if (result.saved.length > 0) { - console.log( - `Saved ${result.saved.length} file(s) to ${this.sessionState.outputDir}` - ); + console.log(`Saved ${result.saved.length} file(s) to ${this.sessionState.outputDir}`); } if (Object.keys(result.failed).length > 0) { - console.warn( - `Failed to save ${Object.keys(result.failed).length} file(s)`, - result.failed - ); + console.warn(`Failed to save ${Object.keys(result.failed).length} file(s)`, result.failed); } } } else { if (this.sessionState.parentExecEnv) { const execEnv = this.sessionState.execEnv; if (execEnv) { - await execEnv.saveOutputFiles( - paths, - this.sessionState.outputDir, - this.sessionState.parentExecEnv - ); + await execEnv.saveOutputFiles(paths, this.sessionState.outputDir, this.sessionState.parentExecEnv); } } } diff --git a/src/core/models.ts b/src/core/models.ts index e0e1560..923d371 100644 --- a/src/core/models.ts +++ b/src/core/models.ts @@ -174,7 +174,11 @@ export class TokenUsageMetadata implements Addable { } add(other: TokenUsageMetadata): TokenUsageMetadata { - return new TokenUsageMetadata(this.input + other.input, this.output + other.output, this.reasoning + other.reasoning); + return new TokenUsageMetadata( + this.input + other.input, + this.output + other.output, + this.reasoning + other.reasoning + ); } toJSON() { @@ -210,10 +214,7 @@ export class ToolUseCountMetadata implements Addable { * @param prefix Prefix for nested metadata keys (used for sub-agents) * @returns Aggregated metadata dictionary */ -export function aggregateMetadata( - metadata: Record, - prefix: string = '' -): Record { +export function aggregateMetadata(metadata: Record, prefix: string = ''): Record { const result: Record = {}; for (const [key, values] of Object.entries(metadata)) { diff --git a/src/index.ts b/src/index.ts index 3856649..138af0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,11 +72,7 @@ export { } from './content/processors.js'; // Async utilities -export { - AsyncContext, - AsyncContextWithReset, - type ContextToken, -} from './utils/context.js'; +export { AsyncContext, AsyncContextWithReset, type ContextToken } from './utils/context.js'; export { AsyncDisposableStack, @@ -116,10 +112,24 @@ export { export { SubAgentMetadata, SubAgentParamsSchema, type SubAgentParams } from './core/sub-agent.js'; // Session management -export { sessionContext, parentDepthContext, createSessionState, getCurrentSession, getParentDepth, type SessionState } from './core/session.js'; +export { + sessionContext, + parentDepthContext, + createSessionState, + getCurrentSession, + getParentDepth, + type SessionState, +} from './core/session.js'; // Tools -export { DEFAULT_TOOLS, SIMPLE_FINISH_TOOL, CALCULATOR_TOOL, USER_INPUT_TOOL, type FinishParams, type UserInputParams } from './tools/index.js'; +export { + DEFAULT_TOOLS, + SIMPLE_FINISH_TOOL, + CALCULATOR_TOOL, + USER_INPUT_TOOL, + type FinishParams, + type UserInputParams, +} from './tools/index.js'; export { WebToolProvider, WebFetchMetadata, WebSearchMetadata } from './tools/web/provider.js'; export { CodeExecToolProvider, @@ -135,7 +145,12 @@ export { export { MCPToolProvider, type McpConfig, McpConfigSchema } from './tools/mcp/index.js'; // Logging -export { type AgentLoggerBase, AgentLogger, createStructuredLogger, type StructuredLoggerOptions } from './utils/logging/index.js'; +export { + type AgentLoggerBase, + AgentLogger, + createStructuredLogger, + type StructuredLoggerOptions, +} from './utils/logging/index.js'; // Prompts export { BASE_SYSTEM_PROMPT, MESSAGE_SUMMARIZER_PROMPT, MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE } from './prompts/index.js'; diff --git a/src/skills/index.ts b/src/skills/index.ts index a24382a..04b204b 100644 --- a/src/skills/index.ts +++ b/src/skills/index.ts @@ -4,8 +4,3 @@ export type { SkillMetadata } from './skills.js'; export { formatSkillsSection, loadSkillsMetadata, parseFrontmatter } from './skills.js'; - - - - - diff --git a/src/skills/skills.ts b/src/skills/skills.ts index 5f46063..54c0885 100644 --- a/src/skills/skills.ts +++ b/src/skills/skills.ts @@ -101,8 +101,3 @@ export function formatSkillsSection(skills: SkillMetadata[]): string { return lines.join('\n'); } - - - - - diff --git a/src/tools/code-exec/docker.ts b/src/tools/code-exec/docker.ts index bf11244..96c9ac6 100644 --- a/src/tools/code-exec/docker.ts +++ b/src/tools/code-exec/docker.ts @@ -142,15 +142,19 @@ export class DockerCodeExecToolProvider extends CodeExecToolProvider { // Collect output with timeout const outputPromise = new Promise((resolve) => { - this.container!.modem.demuxStream(stream, { - write: (chunk: Buffer) => { - stdout += chunk.toString(); - }, - } as any, { - write: (chunk: Buffer) => { - stderr += chunk.toString(); - }, - } as any); + this.container!.modem.demuxStream( + stream, + { + write: (chunk: Buffer) => { + stdout += chunk.toString(); + }, + } as any, + { + write: (chunk: Buffer) => { + stderr += chunk.toString(); + }, + } as any + ); stream.on('end', resolve); }); @@ -158,9 +162,7 @@ export class DockerCodeExecToolProvider extends CodeExecToolProvider { // Race between output collection and timeout await Promise.race([ outputPromise, - new Promise((_, reject) => - setTimeout(() => reject(new Error('timeout')), timeout) - ), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)), ]); // Get exit code diff --git a/src/tools/code-exec/index.ts b/src/tools/code-exec/index.ts index b5abdc0..1e55e39 100644 --- a/src/tools/code-exec/index.ts +++ b/src/tools/code-exec/index.ts @@ -2,7 +2,12 @@ * Code execution exports */ -export { CodeExecToolProvider, CodeExecutionParamsSchema, type CodeExecutionParams, type CommandResult } from './base.js'; +export { + CodeExecToolProvider, + CodeExecutionParamsSchema, + type CodeExecutionParams, + type CommandResult, +} from './base.js'; export { LocalCodeExecToolProvider } from './local.js'; export { DockerCodeExecToolProvider, type DockerCodeExecConfig } from './docker.js'; export { E2BCodeExecToolProvider, type E2BCodeExecConfig } from './e2b.js'; diff --git a/src/tools/code-exec/local.ts b/src/tools/code-exec/local.ts index a4cd341..85941c8 100644 --- a/src/tools/code-exec/local.ts +++ b/src/tools/code-exec/local.ts @@ -20,7 +20,8 @@ export class LocalCodeExecToolProvider extends CodeExecToolProvider { constructor(allowedCommands?: string[], _tempBaseDir?: string, description?: string) { super( allowedCommands, - description ?? 'Execute a shell command in the execution environment. Returns exit code, stdout, and stderr as XML. Use `uv` to manage packages.' + description ?? + 'Execute a shell command in the execution environment. Returns exit code, stdout, and stderr as XML. Use `uv` to manage packages.' ); } diff --git a/src/tools/finish.ts b/src/tools/finish.ts index 9cfdd01..0434bfc 100644 --- a/src/tools/finish.ts +++ b/src/tools/finish.ts @@ -10,7 +10,9 @@ import { ToolUseCountMetadata } from '../core/models.js'; * Parameters for the finish tool */ export const FinishParamsSchema = z.object({ - reason: z.string().describe('Result of the task, including a summary of what was accomplished and the final answer (if applicable)'), + reason: z + .string() + .describe('Result of the task, including a summary of what was accomplished and the final answer (if applicable)'), paths: z .union([z.string(), z.array(z.string())]) .default([]) @@ -33,7 +35,9 @@ export const FinishParamsSchema = z.object({ return arr; }) - .describe('Output file paths (can be a single string or array of strings). Example: ["output.png", "data.csv"] or "output.png"'), + .describe( + 'Output file paths (can be a single string or array of strings). Example: ["output.png", "data.csv"] or "output.png"' + ), }); export type FinishParams = z.infer; diff --git a/src/tools/index.ts b/src/tools/index.ts index 312d825..d6073a8 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -6,7 +6,12 @@ export { SIMPLE_FINISH_TOOL, FinishParamsSchema, type FinishParams } from './fin export { CALCULATOR_TOOL, CalculatorParamsSchema, type CalculatorParams } from './calculator.js'; export { USER_INPUT_TOOL, UserInputParamsSchema, type UserInputParams } from './user-input.js'; export { WebToolProvider, WebFetchMetadata, WebSearchMetadata } from './web/provider.js'; -export { CodeExecToolProvider, LocalCodeExecToolProvider, DockerCodeExecToolProvider, E2BCodeExecToolProvider } from './code-exec/index.js'; +export { + CodeExecToolProvider, + LocalCodeExecToolProvider, + DockerCodeExecToolProvider, + E2BCodeExecToolProvider, +} from './code-exec/index.js'; /** * Default tools for agents @@ -15,7 +20,4 @@ export { CodeExecToolProvider, LocalCodeExecToolProvider, DockerCodeExecToolProv import { WebToolProvider } from './web/provider.js'; import { LocalCodeExecToolProvider } from './code-exec/index.js'; -export const DEFAULT_TOOLS = [ - new LocalCodeExecToolProvider(), - new WebToolProvider(), -]; +export const DEFAULT_TOOLS = [new LocalCodeExecToolProvider(), new WebToolProvider()]; diff --git a/src/tools/mcp/provider.ts b/src/tools/mcp/provider.ts index 461a328..7f66160 100644 --- a/src/tools/mcp/provider.ts +++ b/src/tools/mcp/provider.ts @@ -87,12 +87,15 @@ export class MCPToolProvider implements ToolProvider { env: serverConfig.config.env, }); - const client = new Client({ - name: 'stirrup-client', - version: '1.0.0', - }, { - capabilities: {}, - }); + const client = new Client( + { + name: 'stirrup-client', + version: '1.0.0', + }, + { + capabilities: {}, + } + ); await client.connect(transport); @@ -120,9 +123,7 @@ export class MCPToolProvider implements ToolProvider { */ private createToolFromMcp(mcpTool: any, client: Client, serverName: string): Tool { // Convert JSON Schema to Zod schema - const zodSchema = mcpTool.inputSchema - ? this.jsonSchemaToZod(mcpTool.inputSchema) - : z.object({}); + const zodSchema = mcpTool.inputSchema ? this.jsonSchemaToZod(mcpTool.inputSchema) : z.object({}); // Create tool with prefixed name const toolName = `${serverName}__${mcpTool.name}`; @@ -136,7 +137,7 @@ export class MCPToolProvider implements ToolProvider { // Call MCP tool const result = await client.callTool({ name: mcpTool.name, - arguments: params as any, + arguments: params, }); // Format response as XML @@ -149,7 +150,7 @@ export class MCPToolProvider implements ToolProvider { } else if (item.type === 'image') { content += ` ${item.data}\n`; } else if (item.type === 'resource') { - content += ` ${(item as any).text || ''}\n`; + content += ` ${item.text || ''}\n`; } } } diff --git a/src/tools/user-input.ts b/src/tools/user-input.ts index 6ac88f6..feae9dd 100644 --- a/src/tools/user-input.ts +++ b/src/tools/user-input.ts @@ -9,25 +9,27 @@ import { z } from 'zod'; import type { Tool, ToolResult } from '../core/models.js'; import { ToolUseCountMetadata } from '../core/models.js'; -export const UserInputParamsSchema = z.object({ - question: z.string().min(1).describe('A single question to ask the user (*not* multiple questions)'), - questionType: z - .enum(['text', 'choice', 'confirm']) - .default('text') - .describe("Type of question: 'text' for free-form, 'choice' for multiple choice, 'confirm' for yes/no"), - choices: z.array(z.string()).optional().describe("List of valid choices (required when questionType is 'choice')"), - default: z.string().default('').describe('Default value if user presses Enter without input'), -}).superRefine((val, ctx) => { - if (val.questionType === 'choice') { - if (!val.choices || val.choices.length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['choices'], - message: "choices is required when questionType is 'choice'", - }); +export const UserInputParamsSchema = z + .object({ + question: z.string().min(1).describe('A single question to ask the user (*not* multiple questions)'), + questionType: z + .enum(['text', 'choice', 'confirm']) + .default('text') + .describe("Type of question: 'text' for free-form, 'choice' for multiple choice, 'confirm' for yes/no"), + choices: z.array(z.string()).optional().describe("List of valid choices (required when questionType is 'choice')"), + default: z.string().default('').describe('Default value if user presses Enter without input'), + }) + .superRefine((val, ctx) => { + if (val.questionType === 'choice') { + if (!val.choices || val.choices.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['choices'], + message: "choices is required when questionType is 'choice'", + }); + } } - } -}); + }); export type UserInputParams = z.infer; @@ -105,5 +107,3 @@ export const USER_INPUT_TOOL: Tool(fn: (stack: AsyncDisposableStack) => Pro * @param create Function that creates and returns a resource with cleanup * @returns Wrapped function that returns an AsyncDisposable */ -export function makeAsyncDisposable( - create: () => Promise<[T, AsyncDisposeFn]> -): () => Promise { +export function makeAsyncDisposable(create: () => Promise<[T, AsyncDisposeFn]>): () => Promise { return async () => { const [resource, cleanup] = await create(); return Object.assign(resource as object, { diff --git a/src/utils/logging/structured-logger.ts b/src/utils/logging/structured-logger.ts index 4f4e1e2..09d88ed 100644 --- a/src/utils/logging/structured-logger.ts +++ b/src/utils/logging/structured-logger.ts @@ -24,10 +24,7 @@ export interface StructuredLoggerOptions { * Create a console logger with clean, readable output * @internal */ -function createConsoleLogger( - agent: Agent, - level: string -): () => void { +function createConsoleLogger(agent: Agent, level: string): () => void { const runData: { startTime?: number; agentName?: string; @@ -48,7 +45,9 @@ function createConsoleLogger( } else { taskStr = '[task]'; } - console.log(`${prefix}🚀 Starting ${data.depth > 0 ? 'sub-' : ''}agent${data.depth > 0 ? ` [${runData.agentName}]` : ''}...`); + console.log( + `${prefix}🚀 Starting ${data.depth > 0 ? 'sub-' : ''}agent${data.depth > 0 ? ` [${runData.agentName}]` : ''}...` + ); if (level === 'debug' || level === 'trace') { console.log(`${prefix} Task: ${taskStr}`); } @@ -68,14 +67,17 @@ function createConsoleLogger( // Show assistant message if verbose if (level === 'trace' || level === 'debug') { if (data.content && data.content.length > 0) { - const truncated = data.content.length > MAX_MESSAGE_LENGTH ? data.content.substring(0, MAX_MESSAGE_LENGTH) + '...' : data.content; + const truncated = + data.content.length > MAX_MESSAGE_LENGTH + ? data.content.substring(0, MAX_MESSAGE_LENGTH) + '...' + : data.content; console.log(`${prefix} 💬 ${truncated}`); } } // Show tool calls (more important) if (data.toolCalls && data.toolCalls.length > 0) { - const toolNames = data.toolCalls.map(tc => tc.name).join(', '); + const toolNames = data.toolCalls.map((tc) => tc.name).join(', '); console.log(`${prefix} 🔧 Calling: ${toolNames}`); } }; @@ -144,7 +146,7 @@ function createConsoleLogger( const tools = Object.entries(data.result.runMetadata) .filter(([key]) => key !== 'token_usage') .map(([name, toolData]: [string, any]) => ({ name, uses: toolData.numUses || 0 })) - .filter(t => t.uses > 0); + .filter((t) => t.uses > 0); console.log('╭─ Tool Usage ' + '─'.repeat(65) + '╮'); if (tools.length > 0) { @@ -235,12 +237,7 @@ export function createStructuredLogger( agent: Agent, options: StructuredLoggerOptions = {} ): () => void { - const { - level = 'debug', - pretty = true, - useConsoleFormat = true, - pinoOptions = {}, - } = options; + const { level = 'debug', pretty = true, useConsoleFormat = true, pinoOptions = {} } = options; // Use clean console format by default if (useConsoleFormat) { @@ -268,112 +265,148 @@ export function createStructuredLogger( [K in keyof AgentEvents]: AgentEvents[K]; } = { 'run:start': (data) => { - logger.info({ - event: 'run:start', - task: typeof data.task === 'string' ? data.task.substring(0, MAX_MESSAGE_LENGTH) : '[complex]', - depth: data.depth, - }, 'Agent run started'); + logger.info( + { + event: 'run:start', + task: typeof data.task === 'string' ? data.task.substring(0, MAX_MESSAGE_LENGTH) : '[complex]', + depth: data.depth, + }, + 'Agent run started' + ); }, 'run:complete': (data) => { - logger.info({ - event: 'run:complete', - duration: data.duration, - messageGroups: data.result.messageHistory.length, - tokenUsage: data.result.runMetadata.token_usage, - finishParams: data.result.finishParams, - }, `Agent run completed in ${data.duration}ms`); + logger.info( + { + event: 'run:complete', + duration: data.duration, + messageGroups: data.result.messageHistory.length, + tokenUsage: data.result.runMetadata.token_usage, + finishParams: data.result.finishParams, + }, + `Agent run completed in ${data.duration}ms` + ); }, 'run:error': (data) => { - logger.error({ - event: 'run:error', - error: { - message: data.error.message, - stack: data.error.stack, - name: data.error.name, + logger.error( + { + event: 'run:error', + error: { + message: data.error.message, + stack: data.error.stack, + name: data.error.name, + }, + duration: data.duration, }, - duration: data.duration, - }, `Agent run failed: ${data.error.message}`); + `Agent run failed: ${data.error.message}` + ); }, 'turn:start': (data) => { - logger.debug({ - event: 'turn:start', - turn: data.turn + 1, - maxTurns: data.maxTurns, - progress: `${data.turn + 1}/${data.maxTurns}`, - }, `Turn ${data.turn + 1}/${data.maxTurns} started`); + logger.debug( + { + event: 'turn:start', + turn: data.turn + 1, + maxTurns: data.maxTurns, + progress: `${data.turn + 1}/${data.maxTurns}`, + }, + `Turn ${data.turn + 1}/${data.maxTurns} started` + ); }, 'turn:complete': (data) => { - logger.debug({ - event: 'turn:complete', - turn: data.turn + 1, - tokenUsage: data.tokenUsage, - }, `Turn ${data.turn + 1} completed`); + logger.debug( + { + event: 'turn:complete', + turn: data.turn + 1, + tokenUsage: data.tokenUsage, + }, + `Turn ${data.turn + 1} completed` + ); }, 'message:assistant': (data) => { - logger.trace({ - event: 'message:assistant', - content: data.content.substring(0, MAX_MESSAGE_LENGTH), - toolCalls: data.toolCalls?.map(tc => tc.name) || [], - }, 'Assistant message'); + logger.trace( + { + event: 'message:assistant', + content: data.content.substring(0, MAX_MESSAGE_LENGTH), + toolCalls: data.toolCalls?.map((tc) => tc.name) || [], + }, + 'Assistant message' + ); }, 'message:tool': (data) => { - logger.trace({ - event: 'message:tool', - toolName: data.name, - success: data.success, - content: data.content.substring(0, MAX_MESSAGE_LENGTH), - }, `Tool message: ${data.name}`); + logger.trace( + { + event: 'message:tool', + toolName: data.name, + success: data.success, + content: data.content.substring(0, MAX_MESSAGE_LENGTH), + }, + `Tool message: ${data.name}` + ); }, 'tool:start': (data) => { - logger.debug({ - event: 'tool:start', - toolName: data.name, - arguments: data.arguments, - }, `Executing tool: ${data.name}`); + logger.debug( + { + event: 'tool:start', + toolName: data.name, + arguments: data.arguments, + }, + `Executing tool: ${data.name}` + ); }, 'tool:complete': (data) => { - logger.debug({ - event: 'tool:complete', - toolName: data.name, - success: data.success, - resultLength: data.result.length, - }, `Tool ${data.success ? 'succeeded' : 'failed'}: ${data.name}`); + logger.debug( + { + event: 'tool:complete', + toolName: data.name, + success: data.success, + resultLength: data.result.length, + }, + `Tool ${data.success ? 'succeeded' : 'failed'}: ${data.name}` + ); }, 'tool:error': (data) => { - logger.warn({ - event: 'tool:error', - toolName: data.name, - error: { - message: data.error.message, - name: data.error.name, + logger.warn( + { + event: 'tool:error', + toolName: data.name, + error: { + message: data.error.message, + name: data.error.name, + }, }, - }, `Tool error: ${data.name} - ${data.error.message}`); + `Tool error: ${data.name} - ${data.error.message}` + ); }, 'summarization:start': (data) => { - logger.info({ - event: 'summarization:start', - percentUsed: Math.round(data.percentUsed * 100), - messageCount: data.messageCount, - }, `Context summarization started (${Math.round(data.percentUsed * 100)}% used)`); + logger.info( + { + event: 'summarization:start', + percentUsed: Math.round(data.percentUsed * 100), + messageCount: data.messageCount, + }, + `Context summarization started (${Math.round(data.percentUsed * 100)}% used)` + ); }, 'summarization:complete': (data) => { - logger.info({ - event: 'summarization:complete', - summaryLength: data.summaryLength, - originalCount: data.originalCount, - reduction: `${data.originalCount} → ${data.summaryLength}`, - }, `Context summarized: ${data.originalCount} → ${data.summaryLength} messages`); + logger.info( + { + event: 'summarization:complete', + summaryLength: data.summaryLength, + originalCount: data.originalCount, + reduction: `${data.originalCount} → ${data.summaryLength}`, + }, + `Context summarized: ${data.originalCount} → ${data.summaryLength} messages` + ); }, };