From a2117cb2828284a2c3b507d7192ca20a81fd607a Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 14 Nov 2025 01:46:20 +0600 Subject: [PATCH 1/2] feat: enhance error handling in RunAgentClient and WebSocket clients - Introduced RunAgentExecutionError for structured error reporting. - Updated run() and runStream() methods to throw RunAgentExecutionError on failures. - Enhanced error handling in WebSocket clients to utilize RunAgentExecutionError. - Added suggestions for common error scenarios in REST client responses. - Updated README to reflect new error handling practices. --- runagent-ts/README.md | 19 ++++- runagent-ts/src/client/index.ts | 63 ++++++++++++---- runagent-ts/src/errors/index.ts | 21 +++++- runagent-ts/src/rest/index.ts | 12 ++++ runagent-ts/src/types/index.ts | 1 + runagent-ts/src/websocket/base.ts | 104 ++++++++++++++++++++++++++- runagent-ts/src/websocket/browser.ts | 37 ++++++++-- runagent-ts/src/websocket/node.ts | 37 ++++++++-- 8 files changed, 266 insertions(+), 28 deletions(-) diff --git a/runagent-ts/README.md b/runagent-ts/README.md index c96cfd6..78f7263 100644 --- a/runagent-ts/README.md +++ b/runagent-ts/README.md @@ -55,7 +55,7 @@ For browser builds, you can expose these via bundler secrets or a custom `global ### 1. Remote agent ```ts -import { RunAgentClient } from 'runagent'; +import { RunAgentClient, RunAgentExecutionError } from 'runagent'; const client = new RunAgentClient({ agentId: 'agent-id-from-dashboard', @@ -77,7 +77,7 @@ console.log(result); ### 2. Remote streaming ```ts -import { RunAgentClient } from 'runagent'; +import { RunAgentClient, RunAgentExecutionError } from 'runagent'; const client = new RunAgentClient({ agentId: 'agent-id-from-dashboard', @@ -180,6 +180,21 @@ for await (const chunk of client.runStream({ message: 'Need help' })) { - `getExtraParams(): Record | undefined` Returns any metadata passed in `extraParams`. Reserved for forward compatibility. +- **Errors** + Both `run()` and `runStream()` throw `RunAgentExecutionError` when the agent reports a failure (or when the transport breaks). Inspect `error.code`, `error.message`, and `error.suggestion` to deliver actionable feedback: + + ```ts + try { + const result = await client.run({ prompt: '...' }); + } catch (error) { + if (error instanceof RunAgentExecutionError) { + console.error(error.code, error.message, error.suggestion); + } else { + throw error; + } + } + ``` + ### Constructor precedence rules 1. Explicit constructor values. diff --git a/runagent-ts/src/client/index.ts b/runagent-ts/src/client/index.ts index 0dad822..16fdb17 100644 --- a/runagent-ts/src/client/index.ts +++ b/runagent-ts/src/client/index.ts @@ -12,8 +12,10 @@ import type { RunAgentConfig, AgentArchitecture, JsonValue, + ApiResponse, } from '../types/index.js'; import type { RunAgentRegistry } from '../database/index.js'; +import { RunAgentExecutionError } from '../errors/index.js'; type WebSocketClientType = BrowserWebSocketClient | NodeWebSocketClient; @@ -242,9 +244,10 @@ export class RunAgentClient { } if (!host || !port) { - throw new Error( - `Unable to determine host/port for local agent ${this.agentId}. ` + - `Provide 'host' and 'port' in RunAgentClient config or register the agent locally.` + throw new RunAgentExecutionError( + 'AGENT_ADDRESS_NOT_FOUND', + `Unable to determine host/port for local agent ${this.agentId}`, + "Provide 'host' and 'port' in RunAgentClient config or register the agent locally." ); } @@ -288,7 +291,8 @@ export class RunAgentClient { await this.ensureInitialized(); if (this.entrypointTag.endsWith('_stream')) { - throw new Error( + throw new RunAgentExecutionError( + 'STREAM_ENTRYPOINT', `Entrypoint \`${this.entrypointTag}\` is streaming. Use runStream() instead.` ); } @@ -302,7 +306,8 @@ export class RunAgentClient { await this.ensureInitialized(); if (!this.entrypointTag.endsWith('_stream')) { - throw new Error( + throw new RunAgentExecutionError( + 'NON_STREAM_ENTRYPOINT', `Entrypoint \`${this.entrypointTag}\` is not streaming. Use run() instead.` ); } @@ -358,13 +363,11 @@ export class RunAgentClient { return payload ?? null; } - const errorMessage = - typeof response.error === 'string' - ? response.error - : (response.error as { message?: string })?.message ?? - 'Agent execution failed'; - - throw new Error(errorMessage); + throw this.buildExecutionError( + response.error, + response.message, + 'EXECUTION_ERROR' + ); } private executeStream( @@ -424,4 +427,40 @@ export class RunAgentClient { static async getRegistryInstance(): Promise { return await this.getRegistry(true); } + + private buildExecutionError( + errorInfo: ApiResponse['error'], + fallbackMessage?: string | null, + defaultCode = 'UNKNOWN_ERROR' + ): RunAgentExecutionError { + const fallback = this.sanitizeMessage(fallbackMessage) ?? 'Unknown error'; + + if (!errorInfo) { + return new RunAgentExecutionError(defaultCode, fallback); + } + + if (typeof errorInfo === 'string') { + return new RunAgentExecutionError( + defaultCode, + this.sanitizeMessage(errorInfo) ?? fallback + ); + } + + const { code, message, suggestion, details } = errorInfo; + const normalizedMessage = + this.sanitizeMessage(message) ?? fallback; + + return new RunAgentExecutionError( + code ?? defaultCode, + normalizedMessage, + suggestion, + details + ); + } + + private sanitizeMessage(value?: string | null): string | undefined { + if (!value) return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + } } diff --git a/runagent-ts/src/errors/index.ts b/runagent-ts/src/errors/index.ts index 6d0c8da..7cd5cda 100644 --- a/runagent-ts/src/errors/index.ts +++ b/runagent-ts/src/errors/index.ts @@ -49,4 +49,23 @@ export class HttpException extends Error { super(message, statusCode, response); this.name = 'ConnectionError'; } - } \ No newline at end of file + } + +export class RunAgentExecutionError extends Error { + public code: string; + public suggestion?: string | null; + public details?: unknown; + + constructor( + code: string, + message: string, + suggestion?: string | null, + details?: unknown + ) { + super(message || 'Unknown error'); + this.name = 'RunAgentExecutionError'; + this.code = code || 'UNKNOWN_ERROR'; + this.suggestion = suggestion; + this.details = details; + } +} \ No newline at end of file diff --git a/runagent-ts/src/rest/index.ts b/runagent-ts/src/rest/index.ts index d8176a5..a048cd9 100644 --- a/runagent-ts/src/rest/index.ts +++ b/runagent-ts/src/rest/index.ts @@ -84,6 +84,10 @@ export class RestClient { error: { code, message: errorMessage, + suggestion: + code === 'PERMISSION_ERROR' + ? 'Verify the agent ID and ensure your API key has access to this agent.' + : 'Check that RUNAGENT_API_KEY is set correctly and has not expired.', }, }; } @@ -94,6 +98,8 @@ export class RestClient { error: { code: 'VALIDATION_ERROR', message: errorMessage, + suggestion: + 'Inspect the input arguments and ensure they match the agent entrypoint schema.', }, }; } @@ -104,6 +110,8 @@ export class RestClient { error: { code: 'CONNECTION_ERROR', message: errorMessage, + suggestion: + 'Check your network connection and confirm the RunAgent service URL is reachable.', }, }; } @@ -114,6 +122,7 @@ export class RestClient { error: { code: 'SERVER_ERROR', message: errorMessage, + suggestion: 'Try the request again. If the issue persists, contact RunAgent support.', }, }; } @@ -124,6 +133,8 @@ export class RestClient { error: { code: 'CLIENT_ERROR', message: errorMessage, + suggestion: + 'Review the request payload or configuration for potential mistakes.', }, }; } @@ -133,6 +144,7 @@ export class RestClient { error: { code: 'UNKNOWN_ERROR', message: errorMessage, + suggestion: 'Retry the request or inspect the agent logs for more detail.', }, }; } diff --git a/runagent-ts/src/types/index.ts b/runagent-ts/src/types/index.ts index 82fc24c..0334548 100644 --- a/runagent-ts/src/types/index.ts +++ b/runagent-ts/src/types/index.ts @@ -22,6 +22,7 @@ export interface ApiResponse { | { code?: string; message?: string; + suggestion?: string | null; details?: unknown; field?: string | null; }; diff --git a/runagent-ts/src/websocket/base.ts b/runagent-ts/src/websocket/base.ts index bb1e77c..bbda23f 100644 --- a/runagent-ts/src/websocket/base.ts +++ b/runagent-ts/src/websocket/base.ts @@ -1,5 +1,6 @@ import { CoreSerializer } from '../serializer/index.js'; import type { ExecutionRequest, JsonValue } from '../types/index.js'; +import { RunAgentExecutionError } from '../errors/index.js'; interface WebSocketConfig { baseSocketUrl?: string; @@ -20,6 +21,7 @@ interface StreamMessage { status?: string; payload?: unknown; error?: unknown; + message?: unknown; } export abstract class BaseWebSocketClient { @@ -128,6 +130,14 @@ export abstract class BaseWebSocketClient { ? ((message.data as Record).status as string) : undefined; + const messageField = + message.message ?? + (typeof message.data === 'object' && + message.data !== null && + 'message' in (message.data as Record) + ? (message.data as Record).message + : undefined); + const error = message.error ?? message.detail ?? @@ -144,7 +154,7 @@ export abstract class BaseWebSocketClient { message.delta ?? undefined; - return { type, status, payload, error }; + return { type, status, payload, error, message: messageField }; } protected cleanErrorMessage(error: unknown): string { @@ -212,4 +222,96 @@ export abstract class BaseWebSocketClient { return payload; } + + protected buildStreamError(streamMessage: StreamMessage): RunAgentExecutionError { + const rawError = + streamMessage.error ?? + streamMessage.message ?? + streamMessage.payload ?? + 'Unknown error'; + + const cleanedMessage = this.cleanErrorMessage(rawError); + + let suggestion: string | null | undefined; + let details: unknown; + + const payloadCandidate = + typeof streamMessage.payload === 'object' && streamMessage.payload !== null + ? (streamMessage.payload as Record) + : undefined; + + if (payloadCandidate) { + if (typeof payloadCandidate.message === 'string' && !streamMessage.error) { + const cleanedPayloadMessage = this.cleanErrorMessage( + payloadCandidate.message + ); + if (cleanedPayloadMessage) { + return new RunAgentExecutionError( + payloadCandidate.code as string || 'STREAM_ERROR', + cleanedPayloadMessage, + typeof payloadCandidate.suggestion === 'string' + ? payloadCandidate.suggestion + : undefined, + payloadCandidate.details + ); + } + } + + if (typeof payloadCandidate.suggestion === 'string') { + suggestion = payloadCandidate.suggestion; + } + if ('details' in payloadCandidate) { + details = payloadCandidate.details; + } + } + + return new RunAgentExecutionError( + 'STREAM_ERROR', + cleanedMessage || 'Stream failed', + suggestion, + details + ); + } + + protected handleStatusMessage( + streamMessage: StreamMessage + ): { action: 'continue' } | { action: 'complete' } | { action: 'error'; error: RunAgentExecutionError } { + const status = streamMessage.status?.toLowerCase(); + + if (!status || status === 'stream_started' || status === 'stream_progress' || status === 'stream_update') { + return { action: 'continue' }; + } + + if (status === 'stream_completed') { + return { action: 'complete' }; + } + + if ( + status === 'stream_error' || + status === 'stream_failed' || + status === 'stream_interrupted' + ) { + return { action: 'error', error: this.buildStreamError(streamMessage) }; + } + + if (status === 'stream_retry') { + const message = + typeof streamMessage.payload === 'object' && + streamMessage.payload !== null && + typeof (streamMessage.payload as Record).message === 'string' + ? ((streamMessage.payload as Record).message as string) + : 'Stream temporarily unavailable; retrying'; + + return { + action: 'error', + error: new RunAgentExecutionError( + 'STREAM_RETRY', + this.cleanErrorMessage(message), + 'Retry the request after a short delay.' + ), + }; + } + + return { action: 'continue' }; + } } \ No newline at end of file diff --git a/runagent-ts/src/websocket/browser.ts b/runagent-ts/src/websocket/browser.ts index 03b1f59..e050a0f 100644 --- a/runagent-ts/src/websocket/browser.ts +++ b/runagent-ts/src/websocket/browser.ts @@ -1,4 +1,5 @@ import { BaseWebSocketClient } from './base.js'; +import { RunAgentExecutionError } from '../errors/index.js'; // declare global { @@ -99,15 +100,19 @@ export class BrowserWebSocketClient extends BaseWebSocketClient { const streamMessage = this.parseStreamMessage(event.data as string); if (streamMessage.type === 'status') { - if (streamMessage.status === 'stream_completed') { + const statusAction = this.handleStatusMessage(streamMessage); + if (statusAction.action === 'complete') { finished = true; resolveAll(); + } else if (statusAction.action === 'error') { + error = statusAction.error; + rejectAll(error); } return; } if (streamMessage.type === 'error') { - error = new Error(this.cleanErrorMessage(streamMessage.error)); + error = this.buildStreamError(streamMessage); rejectAll(error); return; } @@ -123,18 +128,38 @@ export class BrowserWebSocketClient extends BaseWebSocketClient { messageQueue.push(payload); } } catch (err) { - error = err instanceof Error ? err : new Error('Unknown error'); - rejectAll(error); + const normalized = + err instanceof RunAgentExecutionError + ? err + : new RunAgentExecutionError( + 'STREAM_ERROR', + this.cleanErrorMessage( + err instanceof Error ? err.message : err ?? 'Unknown error' + ) + ); + error = normalized; + rejectAll(normalized); } }; const closeHandler = () => { + if (!finished && !error) { + error = new RunAgentExecutionError( + 'CONNECTION_ERROR', + 'Stream connection closed unexpectedly' + ); + rejectAll(error); + } else { + resolveAll(); + } finished = true; - resolveAll(); }; const errorHandler = (err: Event) => { - error = new Error(`WebSocket error: ${err}`); + error = new RunAgentExecutionError( + 'CONNECTION_ERROR', + this.cleanErrorMessage(err.toString()) + ); rejectAll(error); }; diff --git a/runagent-ts/src/websocket/node.ts b/runagent-ts/src/websocket/node.ts index d600c9b..0059de4 100644 --- a/runagent-ts/src/websocket/node.ts +++ b/runagent-ts/src/websocket/node.ts @@ -1,4 +1,5 @@ import { BaseWebSocketClient } from './base.js'; +import { RunAgentExecutionError } from '../errors/index.js'; interface IteratorResolverItem { resolve: (value: { done: boolean; value?: unknown }) => void; @@ -96,15 +97,19 @@ export class NodeWebSocketClient extends BaseWebSocketClient { const streamMessage = this.parseStreamMessage(raw); if (streamMessage.type === 'status') { - if (streamMessage.status === 'stream_completed') { + const statusAction = this.handleStatusMessage(streamMessage); + if (statusAction.action === 'complete') { finished = true; resolveAll(); + } else if (statusAction.action === 'error') { + error = statusAction.error; + rejectAll(error); } return; } if (streamMessage.type === 'error') { - error = new Error(this.cleanErrorMessage(streamMessage.error)); + error = this.buildStreamError(streamMessage); rejectAll(error); return; } @@ -120,19 +125,39 @@ export class NodeWebSocketClient extends BaseWebSocketClient { messageQueue.push(payload); } } catch (err) { - error = err instanceof Error ? err : new Error('Unknown error'); - rejectAll(error); + const normalized = + err instanceof RunAgentExecutionError + ? err + : new RunAgentExecutionError( + 'STREAM_ERROR', + this.cleanErrorMessage( + err instanceof Error ? err.message : err ?? 'Unknown error' + ) + ); + error = normalized; + rejectAll(normalized); } }; const closeHandler = () => { + if (!finished && !error) { + error = new RunAgentExecutionError( + 'CONNECTION_ERROR', + 'Stream connection closed unexpectedly' + ); + rejectAll(error); + } else { + resolveAll(); + } finished = true; - resolveAll(); }; const errorHandler = (...args: unknown[]) => { const err = args[0] as Error; - error = new Error(`WebSocket error: ${err.message || 'Unknown error'}`); + error = new RunAgentExecutionError( + 'CONNECTION_ERROR', + this.cleanErrorMessage(err.message || err) + ); rejectAll(error); }; From 4ee31b0067dca3ec2586f8cfa09b52d808b0b30d Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 14 Nov 2025 01:46:48 +0600 Subject: [PATCH 2/2] chore: bump version to v0.1.30 - Updated all SDK versions to 0.1.30 - Generated changelog with git-cliff --- CHANGELOG.md | 6 +++--- pyproject.toml | 6 +++--- runagent-go/runagent/version.go | 2 +- runagent-rust/runagent/Cargo.toml | 2 +- runagent-ts/package-lock.json | 4 ++-- runagent-ts/package.json | 2 +- runagent/__init__.py | 2 +- runagent/__version__.py | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c583285..7388e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog All notable changes to this project's latest version. -## [0.1.28] - 2025-11-13 +## [0.1.29] - 2025-11-13 ### Miscellaneous Tasks -- Update GitHub Actions permissions for release jobs -- Bump version to v0.1.28 +- Update TypeScript release workflow to enhance security +- Bump version to v0.1.29 diff --git a/pyproject.toml b/pyproject.toml index 12a0626..e672c42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "runagent" -version = "0.1.29" +version = "0.1.30" description = "A command-line tool and SDK for deploying, managing, and interacting with AI agents" readme = "README.md" requires-python = ">=3.9" @@ -103,7 +103,7 @@ line_length = 88 skip = ["docs"] [tool.mypy] -python_version = "0.1.29" +python_version = "0.1.30" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -159,7 +159,7 @@ fail_under = 80 [tool.ruff] line-length = 88 -target-version = "0.1.29" +target-version = "0.1.30" select = [ "E", # pycodestyle errors "W", # pycodestyle warnings diff --git a/runagent-go/runagent/version.go b/runagent-go/runagent/version.go index 1431181..c28dc4d 100644 --- a/runagent-go/runagent/version.go +++ b/runagent-go/runagent/version.go @@ -1,4 +1,4 @@ package runagent // Version represents the current version of the RunAgent Go SDK -const Version = "0.1.29" +const Version = "0.1.30" diff --git a/runagent-rust/runagent/Cargo.toml b/runagent-rust/runagent/Cargo.toml index bec1621..de98963 100644 --- a/runagent-rust/runagent/Cargo.toml +++ b/runagent-rust/runagent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runagent" -version = "0.1.29" +version = "0.1.30" edition = "2021" description = "RunAgent SDK for Rust - Client SDK for interacting with deployed AI agents" license = "MIT" diff --git a/runagent-ts/package-lock.json b/runagent-ts/package-lock.json index 83cb179..c91adf4 100644 --- a/runagent-ts/package-lock.json +++ b/runagent-ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "runagent", - "version": "0.1.29", + "version": "0.1.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "runagent", - "version": "0.1.29", + "version": "0.1.30", "dependencies": { "better-sqlite3": "^12.2.0" }, diff --git a/runagent-ts/package.json b/runagent-ts/package.json index df8f531..7c19a39 100644 --- a/runagent-ts/package.json +++ b/runagent-ts/package.json @@ -1,6 +1,6 @@ { "name": "runagent", - "version": "0.1.29", + "version": "0.1.30", "type": "module", "files": [ "dist" diff --git a/runagent/__init__.py b/runagent/__init__.py index 2213a6e..777cff8 100644 --- a/runagent/__init__.py +++ b/runagent/__init__.py @@ -5,7 +5,7 @@ built with frameworks like LangGraph, LangChain, and LlamaIndex. """ -__version__ = "0.1.29" +__version__ = "0.1.30" from .client import RunAgentClient diff --git a/runagent/__version__.py b/runagent/__version__.py index a5f3762..887b2e7 100644 --- a/runagent/__version__.py +++ b/runagent/__version__.py @@ -1 +1 @@ -__version__ = "0.1.29" +__version__ = "0.1.30"