Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/json-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@adcp/client": minor
---

**JSON Logging**: Add `format: 'json'` option to logger for structured JSON output with timestamp, level, message, context, and metadata fields.

```typescript
import { createLogger, SingleAgentClient } from '@adcp/client';

// Create a logger with JSON format for production
const logger = createLogger({ level: 'debug', format: 'json' });

// Pass logger to client for structured logging of task execution
const client = new SingleAgentClient(agentConfig, { logger });
// Output: {"timestamp":"2025-12-13T...","level":"info","message":"Task completed: get_products","context":"TaskExecutor","meta":{"taskId":"...","responseTimeMs":123}}
```

**Injectable Logger Interface**: New `ILogger` interface for dependency injection and `noopLogger` singleton for silent library defaults.

New exports: `ILogger`, `noopLogger`, `LogFormat`
23 changes: 1 addition & 22 deletions src/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ export function generateUUID(): string {
/**
* Get authentication token for an agent
*
* Supports two explicit authentication methods:
* 1. auth_token: Direct token value, used as-is
* 2. auth_token_env: Environment variable name, looked up in process.env
*
* Priority: auth_token takes precedence if both are provided
*
* @param agent - Agent configuration
* @returns Authentication token string or undefined if not configured/required
*/
Expand All @@ -28,28 +22,13 @@ export function getAuthToken(agent: AgentConfig): string | undefined {
return undefined;
}

// Explicit auth_token takes precedence
if (agent.auth_token) {
return agent.auth_token;
}

// Look up auth_token_env in environment
if (agent.auth_token_env) {
const envValue = process.env[agent.auth_token_env];
if (!envValue) {
const message = `Environment variable "${agent.auth_token_env}" not found for agent ${agent.id}`;
if (process.env.NODE_ENV === 'production') {
throw new Error(`[AUTH] ${message} - Agent cannot authenticate`);
} else {
console.warn(`⚠️ ${message}`);
}
}
return envValue;
}

// In production, require explicit auth configuration when requiresAuth is true
if (process.env.NODE_ENV === 'production') {
throw new Error(`[AUTH] Agent ${agent.id} requires authentication but no auth_token or auth_token_env configured`);
throw new Error(`[AUTH] Agent ${agent.id} requires authentication but no auth_token configured`);
}

return undefined;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/core/ADCPMultiAgentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export class ADCPMultiAgentClient {
agentName?: string;
protocol?: 'mcp' | 'a2a';
requiresAuth?: boolean;
authTokenEnv?: string;
authToken?: string;
debug?: boolean;
timeout?: number;
} = {}
Expand All @@ -506,7 +506,7 @@ export class ADCPMultiAgentClient {
agentName = 'Default Agent',
protocol = 'mcp',
requiresAuth = false,
authTokenEnv,
authToken,
debug = false,
timeout,
} = options;
Expand All @@ -517,7 +517,7 @@ export class ADCPMultiAgentClient {
agent_uri: agentUrl,
protocol,
requiresAuth,
auth_token_env: authTokenEnv,
auth_token: authToken,
};

ConfigurationManager.validateAgentConfig(agent);
Expand Down
11 changes: 3 additions & 8 deletions src/lib/core/ConfigurationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export class ConfigurationManager {
agent_uri: 'https://premium-ads.example.com/mcp/',
protocol: 'mcp',
requiresAuth: true,
auth_token_env: 'PREMIUM_AGENT_TOKEN',
auth_token: process.env.PREMIUM_AGENT_TOKEN,
},
{
id: 'budget-network',
Expand Down Expand Up @@ -277,8 +277,7 @@ The ADCP client can load agents from multiple sources:
"name": "Premium Ad Network",
"agent_uri": "https://premium.example.com",
"protocol": "mcp",
"requiresAuth": true,
"auth_token_env": "PREMIUM_TOKEN"
"requiresAuth": true
},
{
"id": "dev-agent",
Expand All @@ -291,14 +290,10 @@ The ADCP client can load agents from multiple sources:
]
}

Authentication options:
- auth_token_env: Environment variable name (recommended for production)
- auth_token: Direct token value (useful for development/testing)

3️⃣ Programmatic Configuration:
const client = new ADCPMultiAgentClient([
{ id: 'agent', agent_uri: 'https://...', protocol: 'mcp',
auth_token_env: 'MY_TOKEN' }
auth_token: process.env.MY_TOKEN }
]);

📖 For more examples, see the documentation.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/CreativeAgentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class CreativeAgentClient {
name: 'Creative Agent',
agent_uri: config.agentUrl,
protocol: config.protocol || 'mcp',
...(config.authToken && { auth_token_env: config.authToken }),
...(config.authToken && { auth_token: config.authToken }),
};

this.client = new SingleAgentClient(agentConfig, config);
Expand Down
10 changes: 7 additions & 3 deletions src/lib/core/SingleAgentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { z } from 'zod';
import * as schemas from '../types/schemas.generated';
import type { AgentConfig } from '../types';
import type { ILogger } from '../utils/logger';
import type {
GetProductsRequest,
GetProductsResponse,
Expand Down Expand Up @@ -44,6 +45,8 @@ import * as crypto from 'crypto';
export interface SingleAgentClientConfig extends ConversationConfig {
/** Enable debug logging */
debug?: boolean;
/** Logger instance for structured logging (use createLogger() from @adcp/client) */
logger?: ILogger;
/** Custom user agent string */
userAgent?: string;
/** Additional headers to include in requests */
Expand Down Expand Up @@ -133,6 +136,7 @@ export class SingleAgentClient {
strictSchemaValidation: config.validation?.strictSchemaValidation !== false, // Default: true
logSchemaViolations: config.validation?.logSchemaViolations !== false, // Default: true
onActivity: config.onActivity,
logger: config.logger,
});

// Create async handler if handlers are provided
Expand Down Expand Up @@ -185,7 +189,7 @@ export class SingleAgentClient {
const { Client: MCPClient } = await import('@modelcontextprotocol/sdk/client/index.js');
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');

const authToken = this.agent.auth_token_env;
const authToken = this.agent.auth_token;

const testEndpoint = async (url: string): Promise<boolean> => {
try {
Expand Down Expand Up @@ -1142,7 +1146,7 @@ export class SingleAgentClient {
version: '1.0.0',
});

const authToken = this.agent.auth_token_env;
const authToken = this.agent.auth_token;
const customFetch = authToken
? async (input: any, init?: any) => {
// IMPORTANT: Must preserve SDK's default headers (especially Accept header)
Expand Down Expand Up @@ -1208,7 +1212,7 @@ export class SingleAgentClient {
const clientModule = require('@a2a-js/sdk/client');
const A2AClient = clientModule.A2AClient;

const authToken = this.agent.auth_token_env;
const authToken = this.agent.auth_token;
const fetchImpl = authToken
? async (url: any, options?: any) => {
const headers = {
Expand Down
29 changes: 24 additions & 5 deletions src/lib/core/TaskExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ProtocolClient } from '../protocols';
import type { Storage } from '../storage/interfaces';
import { responseValidator } from './ResponseValidator';
import { unwrapProtocolResponse } from '../utils/response-unwrapper';
import type { ILogger } from '../utils/logger';
import { noopLogger } from '../utils/logger';
import type {
Message,
InputRequest,
Expand Down Expand Up @@ -83,6 +85,7 @@ export class TaskExecutor {
private responseParser: ProtocolResponseParser;
private activeTasks = new Map<string, TaskState>();
private conversationStorage?: Map<string, Message[]>;
private logger: ILogger;

constructor(
private config: {
Expand Down Expand Up @@ -110,9 +113,12 @@ export class TaskExecutor {
logSchemaViolations?: boolean;
/** Global activity callback for observability */
onActivity?: (activity: Activity) => void | Promise<void>;
/** Logger for debug/info/warn/error messages (default: noopLogger) */
logger?: ILogger;
} = {}
) {
this.responseParser = new ProtocolResponseParser();
this.logger = (config.logger || noopLogger).child('TaskExecutor');
if (config.enableConversationStorage) {
this.conversationStorage = new Map();
}
Expand Down Expand Up @@ -147,6 +153,8 @@ export class TaskExecutor {
const startTime = Date.now();
const workingTimeout = this.config.workingTimeout || 120000; // 120s max per PR #78

this.logger.debug(`Executing task: ${taskName}`, { taskId, agent: agent.id, protocol: agent.protocol });

// Register task in active tasks
const taskState: TaskState = {
taskId,
Expand Down Expand Up @@ -272,6 +280,7 @@ export class TaskExecutor {
startTime: number = Date.now()
): Promise<TaskResult<T>> {
const status = this.responseParser.getStatus(response) as ADCPStatus;
this.logger.debug(`Response status: ${status}`, { taskId, taskName });

switch (status) {
case ADCP_STATUS.COMPLETED:
Expand All @@ -295,6 +304,12 @@ export class TaskExecutor {
: completedData?.error || completedData?.message || 'Operation failed'
: undefined;

if (finalSuccess) {
this.logger.info(`Task completed: ${taskName}`, { taskId, responseTimeMs: Date.now() - startTime });
} else {
this.logger.warn(`Task completed with error: ${finalError}`, { taskId, taskName });
}

return {
success: finalSuccess,
status: 'completed',
Expand Down Expand Up @@ -681,7 +696,7 @@ export class TaskExecutor {
};
}

// Handler provided input - continue with the task
// Handler provided input - continue with the task (pass inputHandler for multi-round)
return this.continueTaskWithInput<T>(
agent,
taskId,
Expand All @@ -690,6 +705,7 @@ export class TaskExecutor {
response.contextId,
handlerResponse,
messages,
inputHandler,
options,
debugLogs,
startTime
Expand Down Expand Up @@ -760,15 +776,16 @@ export class TaskExecutor {
throw new Error(`Deferred task not found: ${token}`);
}

// Continue task with the provided input
// Continue task with the provided input (no handler for manual resume)
return this.continueTaskWithInput<T>(
state.agent,
state.taskId,
state.taskName,
state.params,
state.contextId,
input,
state.messages
state.messages,
undefined
);
}

Expand All @@ -783,6 +800,7 @@ export class TaskExecutor {
contextId: string,
input: any,
messages: Message[],
inputHandler?: InputHandler,
options: TaskOptions = {},
debugLogs: any[] = [],
startTime: number = Date.now()
Expand Down Expand Up @@ -818,15 +836,15 @@ export class TaskExecutor {
};
messages.push(responseMessage);

// Handle the continued response
// Handle the continued response (pass inputHandler for multi-round conversations)
return this.handleAsyncResponse<T>(
agent,
taskId,
taskName,
params,
response,
messages,
undefined,
inputHandler,
options,
debugLogs,
startTime
Expand All @@ -847,6 +865,7 @@ export class TaskExecutor {
debugLogs: any[] = [],
startTime: number = Date.now()
): TaskResult<T> {
this.logger.error(`Task failed: ${error.message || error}`, { taskId, agent: agent.id });
return {
success: false,
status: 'completed', // TaskResult status
Expand Down
2 changes: 1 addition & 1 deletion src/lib/discovery/property-crawler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class PropertyCrawler {
name: 'Property Crawler',
agent_uri: agentInfo.agent_url,
protocol: agentInfo.protocol || 'mcp',
...(agentInfo.auth_token && { auth_token_env: agentInfo.auth_token }),
...(agentInfo.auth_token && { auth_token: agentInfo.auth_token }),
});

try {
Expand Down
11 changes: 11 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ export { getStandardFormats, unwrapProtocolResponse, isAdcpError, isAdcpSuccess
export { REQUEST_TIMEOUT, MAX_CONCURRENT, STANDARD_FORMATS } from './utils';
export { detectProtocol, detectProtocolWithTimeout } from './utils';

// ====== LOGGING ======
// Logger utilities for production deployments
export {
createLogger,
noopLogger,
type ILogger,
type LoggerConfig,
type LogFormat,
type LogLevel,
} from './utils/logger';

// ====== VERSION INFORMATION ======
export {
getAdcpVersion,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/testing/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const TEST_AGENT_MCP_CONFIG: AgentConfig = {
name: 'AdCP Public Test Agent (MCP)',
agent_uri: 'https://test-agent.adcontextprotocol.org/mcp/',
protocol: 'mcp',
auth_token_env: TEST_AGENT_TOKEN,
auth_token: TEST_AGENT_TOKEN,
requiresAuth: true,
};

Expand All @@ -34,7 +34,7 @@ export const TEST_AGENT_A2A_CONFIG: AgentConfig = {
name: 'AdCP Public Test Agent (A2A)',
agent_uri: 'https://test-agent.adcontextprotocol.org',
protocol: 'a2a',
auth_token_env: TEST_AGENT_TOKEN,
auth_token: TEST_AGENT_TOKEN,
requiresAuth: true,
};

Expand Down
4 changes: 1 addition & 3 deletions src/lib/types/adcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,8 @@ export interface AgentConfig {
name: string;
agent_uri: string;
protocol: 'mcp' | 'a2a';
/** Direct authentication token value */
/** Authentication token value */
auth_token?: string;
/** Environment variable name containing the auth token */
auth_token_env?: string;
requiresAuth?: boolean;
}

Expand Down
Loading