diff --git a/sdk/typescript/src/agent/Agent.ts b/sdk/typescript/src/agent/Agent.ts index 99798e304..91571acab 100644 --- a/sdk/typescript/src/agent/Agent.ts +++ b/sdk/typescript/src/agent/Agent.ts @@ -9,7 +9,8 @@ import type { DeploymentType, HealthStatus, ServerlessEvent, - ServerlessResponse + ServerlessResponse, + RawExecutionContext } from '../types/agent.js'; import { ReasonerRegistry } from './ReasonerRegistry.js'; import { SkillRegistry } from './SkillRegistry.js'; @@ -43,16 +44,43 @@ import { type ExecutionLogger } from '../observability/ExecutionLogger.js'; import { LocalVerifier } from '../verification/LocalVerifier.js'; +import type { Request, Response } from 'express'; +import type { ParamsDictionary } from 'express-serve-static-core'; import { installStdioLogCapture, ProcessLogRing, registerAgentfieldLogsRoute } from './processLogs.js'; +interface WildcardParams extends ParamsDictionary { + 0: string; +} class TargetNotFoundError extends Error {} const harnessRunners = new WeakMap(); + + +function normalizeExecutionContext( + ctx: RawExecutionContext +): Partial { + return { + executionId: ctx.executionId ?? ctx.execution_id, + runId: ctx.runId ?? ctx.run_id, + workflowId: ctx.workflowId ?? ctx.workflow_id, + parentExecutionId: ctx.parentExecutionId ?? ctx.parent_execution_id, + sessionId: ctx.sessionId ?? ctx.session_id, + actorId: ctx.actorId ?? ctx.actor_id, + callerDid: ctx.callerDid ?? ctx.caller_did, + targetDid: ctx.targetDid ?? ctx.target_did, + agentNodeDid: ctx.agentNodeDid ?? ctx.agent_node_did, + + // ✅ ADD THESE + rootWorkflowId: (ctx as any).rootWorkflowId ?? (ctx as any).root_workflow_id, + reasonerId: (ctx as any).reasonerId ?? (ctx as any).reasoner_id + }; +} + export class Agent { readonly config: AgentConfig; readonly app: express.Express; @@ -739,10 +767,10 @@ export class Agent { }); } - this.app.post('/api/v1/reasoners/*', (req, res) => this.executeReasoner(req, res, (req.params as any)[0])); + this.app.post('/api/v1/reasoners/*', (req: Request, res: Response) => this.executeReasoner(req, res, req.params[0])); this.app.post('/reasoners/:name', (req, res) => this.executeReasoner(req, res, req.params.name)); - this.app.post('/api/v1/skills/*', (req, res) => this.executeSkill(req, res, (req.params as any)[0])); + this.app.post('/api/v1/skills/*', (req: Request, res: Response) => this.executeSkill(req, res, req.params[0])); this.app.post('/skills/:name', (req, res) => this.executeSkill(req, res, req.params.name)); // Serverless-friendly execute endpoint that accepts { target, input } or { reasoner, input } @@ -875,7 +903,7 @@ export class Agent { private handleHttpRequest(req: http.IncomingMessage | express.Request, res: http.ServerResponse | express.Response) { const handler = this.app as unknown as (req: http.IncomingMessage, res: http.ServerResponse) => void; - return handler(req as any, res as any); + return handler(req as http.IncomingMessage, res as http.ServerResponse); } private async handleServerlessEvent(event: ServerlessEvent): Promise { @@ -890,7 +918,7 @@ export class Agent { }; } - const body = this.normalizeEventBody(event); + const body = event?.body !== undefined ? this.parseBody(event.body): event; const invocation = this.extractInvocationDetails({ path, query: event?.queryStringParameters, @@ -938,48 +966,23 @@ export class Agent { } private normalizeEventBody(event: ServerlessEvent) { - const parsed = this.parseBody((event as any)?.body); - if (parsed && typeof parsed === 'object' && event?.input !== undefined && (parsed as any).input === undefined) { - return { ...(parsed as Record), input: event.input }; - } - if ((parsed === undefined || parsed === null) && event?.input !== undefined) { - return { input: event.input }; + interface ParsedBody { + input?: unknown; + data?: unknown; + [key: string]: unknown; } + + const parsed = this.parseBody(event?.body) as ParsedBody | undefined; + + if (parsed?.input !== undefined) return parsed.input; + if (parsed?.data !== undefined) return parsed.data; + return parsed; } private mergeExecutionContext(event: ServerlessEvent): Partial { - const ctx = (event?.executionContext ?? (event as any)?.execution_context) as Partial< - ExecutionMetadata & { - execution_id?: string; - run_id?: string; - workflow_id?: string; - root_workflow_id?: string; - parent_execution_id?: string; - reasoner_id?: string; - session_id?: string; - actor_id?: string; - caller_did?: string; - target_did?: string; - agent_node_did?: string; - } - >; - - if (!ctx) return {}; - - return { - executionId: (ctx as any).executionId ?? ctx.execution_id ?? ctx.executionId, - runId: ctx.runId ?? (ctx as any).run_id, - workflowId: ctx.workflowId ?? (ctx as any).workflow_id, - rootWorkflowId: ctx.rootWorkflowId ?? (ctx as any).root_workflow_id, - parentExecutionId: ctx.parentExecutionId ?? (ctx as any).parent_execution_id, - reasonerId: ctx.reasonerId ?? (ctx as any).reasoner_id, - sessionId: ctx.sessionId ?? (ctx as any).session_id, - actorId: ctx.actorId ?? (ctx as any).actor_id, - callerDid: (ctx as any).callerDid ?? (ctx as any).caller_did, - targetDid: (ctx as any).targetDid ?? (ctx as any).target_did, - agentNodeDid: (ctx as any).agentNodeDid ?? (ctx as any).agent_node_did - }; + const rawCtx = event?.executionContext ?? event?.execution_context; + return rawCtx ? normalizeExecutionContext(rawCtx) : {}; } private extractInvocationDetails(params: { @@ -1064,12 +1067,15 @@ export class Agent { if (parsed && typeof parsed === 'object') { const { target, reasoner, skill, type, targetType, ...rest } = parsed as Record; - if ((parsed as any).input !== undefined) { - return (parsed as any).input; - } - if ((parsed as any).data !== undefined) { - return (parsed as any).data; + interface ParsedBody { + input?: any; + data?: any; + [key: string]: any; } + + const parsedBody = parsed as ParsedBody; + if (parsedBody.input !== undefined) return parsedBody.input; + if (parsedBody.data !== undefined) return parsedBody.data; if (Object.keys(rest).length === 0) { return {}; } diff --git a/sdk/typescript/src/ai/RateLimiter.ts b/sdk/typescript/src/ai/RateLimiter.ts index c9a6767c4..3766e2be8 100644 --- a/sdk/typescript/src/ai/RateLimiter.ts +++ b/sdk/typescript/src/ai/RateLimiter.ts @@ -3,11 +3,33 @@ import os from 'node:os'; export class RateLimitError extends Error { retryAfter?: number; - - constructor(message: string, retryAfter?: number) { + status?: number; + statusCode?: number; + response?: { + status?: number; + statusCode?: number; + status_code?: number; + headers?: Record; + }; + + constructor( + message: string, + retryAfter?: number, + status?: number, + statusCode?: number, + response?: { + status?: number; + statusCode?: number; + status_code?: number; + headers?: Record; + } + ) { super(message); this.name = 'RateLimitError'; this.retryAfter = retryAfter; + this.status = status; + this.statusCode = statusCode; + this.response = response; } } @@ -57,26 +79,38 @@ export class StatelessRateLimiter { protected _isRateLimitError(error: unknown): boolean { if (!error) return false; - const err = error as any; - const className = err?.constructor?.name; - if (className && className.includes('RateLimitError')) { + if (error instanceof RateLimitError) { return true; } - const response = err?.response; + if ( + typeof error === 'object' && + error !== null && + typeof (error as { constructor?: { name?: string } }).constructor?.name === 'string' && + (error as { constructor: { name: string } }).constructor.name.includes('RateLimitError') + ) { + return true; + } + + if (isRateLimitError(error)) { const statusCandidates = [ - err?.status, - err?.statusCode, - response?.status, - response?.statusCode, - response?.status_code + error.status, + error.statusCode, + error.response?.status, + error.response?.statusCode, + error.response?.status_code, ]; - if (statusCandidates.some((code: any) => code === 429 || code === 503)) { + if (statusCandidates.some((code) => code === 429 || code === 503)) { return true; } - const message = String(err?.message ?? err ?? '').toLowerCase(); + if (error.retryAfter !== undefined) { + return true; + } + } + const err = toError(error); + const message = err.message.toLowerCase(); const rateLimitKeywords = [ 'rate limit', 'rate-limit', @@ -98,26 +132,40 @@ export class StatelessRateLimiter { protected _extractRetryAfter(error: unknown): number | undefined { if (!error) return undefined; - const err = error as any; + if (error instanceof RateLimitError) { + if (error.retryAfter) { + return error.retryAfter; + } + } - const headers = err?.response?.headers ?? err?.response?.Headers ?? err?.response?.header; + if (isRateLimitError(error)) { + const headers = error.response?.headers; if (headers && typeof headers === 'object') { const retryAfterKey = Object.keys(headers).find((k) => k.toLowerCase() === 'retry-after'); if (retryAfterKey) { const value = Array.isArray(headers[retryAfterKey]) ? headers[retryAfterKey][0] : headers[retryAfterKey]; - const parsed = parseFloat(value); - if (!Number.isNaN(parsed)) { - return parsed; + if (value !== undefined) { + const parsed = parseFloat(String(value)); + if (!Number.isNaN(parsed)) { + return parsed; + } } } } - - const retryAfter = err?.retryAfter ?? err?.retry_after; - const parsed = parseFloat(retryAfter); - if (!Number.isNaN(parsed)) { - return parsed; + if (error.retryAfter !== undefined) { + const parsed = parseFloat(String(error.retryAfter)); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + } + const err = error as { retryAfter?: string | number }; + if (err.retryAfter !== undefined) { + const parsed = parseFloat(String(err.retryAfter)); + if (!Number.isNaN(parsed)) { + return parsed; + } } - return undefined; } @@ -219,3 +267,24 @@ export class StatelessRateLimiter { ); } } + + +function toError(error: unknown): Error { + if (error instanceof Error) return error; + return new Error(String(error)); +} + + +function isRateLimitError(error: unknown): error is RateLimitError { + return ( + typeof error === 'object' && + error !== null && + ('name' in error || 'message' in error) && + ( + 'status' in error || + 'statusCode' in error || + 'response' in error || + 'retryAfter' in error + ) + ); +} diff --git a/sdk/typescript/src/ai/ToolCalling.ts b/sdk/typescript/src/ai/ToolCalling.ts index 1ae065838..f0968cd8d 100644 --- a/sdk/typescript/src/ai/ToolCalling.ts +++ b/sdk/typescript/src/ai/ToolCalling.ts @@ -97,6 +97,12 @@ export interface AIToolRequestOptions extends AIRequestOptions { maxToolCalls?: number; } +type ToolConfig = { + description: string + inputSchema: any; + execute?: (args: Record) => Promise; +} + // --------------------------------------------------------------------------- // Capability -> Tool Definition Conversion // --------------------------------------------------------------------------- @@ -368,7 +374,7 @@ function wrapToolsWithObservability( const observableTools: ToolSet = {}; for (const [name, t] of Object.entries(toolMap)) { - const originalTool = t as any; + const originalTool = t as ToolConfig; observableTools[name] = tool({ description: originalTool.description ?? '', inputSchema: originalTool.inputSchema, @@ -443,7 +449,7 @@ export async function executeToolCallLoop( // Create non-executable tool stubs so the LLM selects but doesn't execute const selectionTools: ToolSet = {}; for (const [name, t] of Object.entries(toolMap)) { - const orig = t as any; + const orig = t as ToolConfig; selectionTools[name] = tool({ description: orig.description ?? '', inputSchema: orig.inputSchema, diff --git a/sdk/typescript/src/client/AgentFieldClient.ts b/sdk/typescript/src/client/AgentFieldClient.ts index 97e607df0..99bf3f291 100644 --- a/sdk/typescript/src/client/AgentFieldClient.ts +++ b/sdk/typescript/src/client/AgentFieldClient.ts @@ -24,6 +24,67 @@ export interface ExecutionStatusUpdate { statusReason?: string; } +// Raw discovery payload from API (snake_case) +interface RawDiscoveryPayload { + discovered_at?: string; + total_agents?: number; + total_reasoners?: number; + total_skills?: number; + pagination?: { + limit?: number; + offset?: number; + has_more?: boolean; + }; + capabilities?: RawCapability[]; +} + +interface RawCapability { + agent_id?: string; + base_url?: string; + version?: string; + health_status?: string; + deployment_type?: string; + last_heartbeat?: string; + reasoners?: RawReasoner[]; + skills?: RawSkill[]; +} + +interface RawReasoner { + id?: string; + description?: string; + tags?: string[]; + input_schema?: any; + output_schema?: any; + examples?: any; + invocation_target?: string; +} + +interface RawSkill { + id?: string; + description?: string; + tags?: string[]; + input_schema?: any; + invocation_target?: string; +} + +// Compact format +interface RawCompactDiscoveryPayload { + discovered_at?: string; + reasoners?: RawCompactCapability[]; + skills?: RawCompactCapability[]; +} + +interface RawCompactCapability { + id?: string; + agent_id?: string; + target?: string; + tags?: string[]; +} + +interface ExecutionError extends Error { + status: number; + responseData: unknown; +} export class AgentFieldClient { private readonly http: AxiosInstance; private readonly config: AgentConfig; @@ -121,9 +182,13 @@ this.http = axios.create({ if (respData) { const status = err.response.status; const msg = respData.message || respData.error || JSON.stringify(respData); - const enriched = new Error(`execute ${target} failed (${status}): ${msg}`); - (enriched as any).status = status; - (enriched as any).responseData = respData; + const enriched: ExecutionError = Object.assign( + new Error(`execute ${target} failed (${status}): ${msg}`), + { + status, + responseData: respData + } + ); throw enriched; } throw err; @@ -277,72 +342,72 @@ this.http = axios.create({ return { format: 'xml', raw, xml: raw }; } - const parsed = typeof res.data === 'string' ? JSON.parse(res.data) : res.data; + const parsed: RawDiscoveryPayload | RawCompactDiscoveryPayload = typeof res.data === 'string' ? JSON.parse(res.data) : res.data; if (format === 'compact') { return { format: 'compact', raw, - compact: this.mapCompactDiscovery(parsed as any) + compact: this.mapCompactDiscovery(parsed as RawCompactDiscoveryPayload) }; } return { format: 'json', raw, - json: this.mapDiscoveryResponse(parsed as any) + json: this.mapDiscoveryResponse(parsed as RawDiscoveryPayload) }; } - private mapDiscoveryResponse(payload: any): DiscoveryResponse { + private mapDiscoveryResponse(payload: RawDiscoveryPayload): DiscoveryResponse { return { - discoveredAt: String(payload?.discovered_at ?? ''), - totalAgents: Number(payload?.total_agents ?? 0), - totalReasoners: Number(payload?.total_reasoners ?? 0), - totalSkills: Number(payload?.total_skills ?? 0), + discoveredAt: String(payload.discovered_at ?? ''), + totalAgents: Number(payload.total_agents ?? 0), + totalReasoners: Number(payload.total_reasoners ?? 0), + totalSkills: Number(payload.total_skills ?? 0), pagination: { - limit: Number(payload?.pagination?.limit ?? 0), - offset: Number(payload?.pagination?.offset ?? 0), - hasMore: Boolean(payload?.pagination?.has_more) + limit: Number(payload.pagination?.limit ?? 0), + offset: Number(payload.pagination?.offset ?? 0), + hasMore: Boolean(payload.pagination?.has_more) }, - capabilities: (payload?.capabilities ?? []).map((cap: any) => ({ - agentId: cap?.agent_id ?? '', - baseUrl: cap?.base_url ?? '', - version: cap?.version ?? '', - healthStatus: cap?.health_status ?? '', - deploymentType: cap?.deployment_type, - lastHeartbeat: cap?.last_heartbeat, - reasoners: (cap?.reasoners ?? []).map((r: any) => ({ - id: r?.id ?? '', - description: r?.description, - tags: r?.tags ?? [], - inputSchema: r?.input_schema, - outputSchema: r?.output_schema, - examples: r?.examples, - invocationTarget: r?.invocation_target ?? '' + capabilities: (payload.capabilities ?? []).map((cap) => ({ + agentId: cap.agent_id ?? '', + baseUrl: cap.base_url ?? '', + version: cap.version ?? '', + healthStatus: cap.health_status ?? '', + deploymentType: cap.deployment_type, + lastHeartbeat: cap.last_heartbeat, + reasoners: (cap.reasoners ?? []).map((r) => ({ + id: r.id ?? '', + description: r.description, + tags: r.tags ?? [], + inputSchema: r.input_schema, + outputSchema: r.output_schema, + examples: r.examples, + invocationTarget: r.invocation_target ?? '' })), - skills: (cap?.skills ?? []).map((s: any) => ({ - id: s?.id ?? '', - description: s?.description, - tags: s?.tags ?? [], - inputSchema: s?.input_schema, - invocationTarget: s?.invocation_target ?? '' + skills: (cap.skills ?? []).map((s) => ({ + id: s.id ?? '', + description: s.description, + tags: s.tags ?? [], + inputSchema: s.input_schema, + invocationTarget: s.invocation_target ?? '' })) })) }; } - private mapCompactDiscovery(payload: any): CompactDiscoveryResponse { - const toCap = (cap: any) => ({ - id: cap?.id ?? '', - agentId: cap?.agent_id ?? '', - target: cap?.target ?? '', - tags: cap?.tags ?? [] + private mapCompactDiscovery(payload: RawCompactDiscoveryPayload): CompactDiscoveryResponse { + const toCap = (cap: RawCompactCapability) => ({ + id: cap.id ?? '', + agentId: cap.agent_id ?? '', + target: cap.target ?? '', + tags: cap.tags ?? [] }); return { - discoveredAt: String(payload?.discovered_at ?? ''), - reasoners: (payload?.reasoners ?? []).map(toCap), - skills: (payload?.skills ?? []).map(toCap) + discoveredAt: String(payload.discovered_at ?? ''), + reasoners: (payload.reasoners ?? []).map(toCap), + skills: (payload.skills ?? []).map(toCap) }; } diff --git a/sdk/typescript/src/types/agent.ts b/sdk/typescript/src/types/agent.ts index f6624966b..50228b32e 100644 --- a/sdk/typescript/src/types/agent.ts +++ b/sdk/typescript/src/types/agent.ts @@ -181,8 +181,8 @@ export interface ServerlessEvent { type?: 'reasoner' | 'skill'; body?: any; input?: any; - executionContext?: Partial; - execution_context?: Partial; + executionContext?: RawExecutionContext; + execution_context?: RawExecutionContext; } export interface ServerlessResponse { @@ -199,3 +199,26 @@ export type AgentHandler = ( ) => Promise | ServerlessResponse | void; export type Awaitable = T | Promise; + +export interface RawExecutionContext { + executionId?: string; + runId?: string; + workflowId?: string; + parentExecutionId?: string; + sessionId?: string; + actorId?: string; + callerDid?: string; + targetDid?: string; + agentNodeDid?: string; + + // snake_case variants + execution_id?: string; + run_id?: string; + workflow_id?: string; + parent_execution_id?: string; + session_id?: string; + actor_id?: string; + caller_did?: string; + target_did?: string; + agent_node_did?: string; +} \ No newline at end of file