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
115 changes: 63 additions & 52 deletions packages/agents-a365-tooling-extensions-claude/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,77 +204,88 @@ private readonly orchestratorName: string = "Claude";

## Chat History API

> **Last Assessed:** January 2026
> **Claude SDK Version:** ^0.1.30 (workspace), `unstable_v2` APIs added in v0.1.54
> **Tracking Issue:** [#164 - Claude SDK: Monitor for chat history API availability](https://github.com/microsoft/Agent365-nodejs/issues/164)
> **Last Assessed:** June 2026
> **Claude SDK Version:** ^0.2.59 (workspace, `getSessionMessages` added in v0.2.59)
> **Tracking Issue:** [#164 - Claude SDK: Monitor for chat history API availability](https://github.com/microsoft/Agent365-nodejs/issues/164) — **Resolved**
Comment thread
gwharris7 marked this conversation as resolved.

### Current State

Unlike the OpenAI extension which provides `sendChatHistoryAsync` via `OpenAIConversationsSession.getItems()`, the Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) **does not expose programmatic access to conversation history**.
The Claude Agent SDK (`@anthropic-ai/claude-agent-sdk` v0.2.59+) now exposes `getSessionMessages(sessionId, options?)` which returns `SessionMessage[]`. This package provides a Claude-specific `sendChatHistoryAsync` method that mirrors the OpenAI extension's implementation from PR #157.

The SDK includes experimental `unstable_v2_*` session APIs (added in v0.1.54):
- `unstable_v2_createSession` - Creates a new session for multi-turn conversations
- `unstable_v2_resumeSession` - Resumes an existing session by ID
- `unstable_v2_prompt` - One-shot convenience function for single-turn queries
### Usage

However, these APIs only provide:
- `session.send(message)` - Send a message to Claude
- `session.stream()` - Stream back response messages
- `session.close()` - Close the session
```typescript
import { McpToolRegistrationService } from '@microsoft/agents-a365-tooling-extensions-claude';

**There is no `session.getHistory()` or equivalent method** to retrieve past messages from a session. Sessions maintain context internally for Claude to reference, but this history is opaque to SDK consumers.
const registrationService = new McpToolRegistrationService();

### Recommended Approach
// Send chat history from a Claude session to MCP platform for real-time threat protection
const result = await registrationService.sendChatHistoryAsync(
turnContext,
'session-abc-123', // Claude session ID
50 // Optional: limit to most recent N messages
);

Developers should use the generic `sendChatHistory` method from `@microsoft/agents-a365-tooling` by manually constructing `ChatHistoryMessage[]`:
if (result.succeeded) {
console.log('Chat history sent successfully');
} else {
console.error('Failed to send chat history:', result.errors);
}
```

### Method Signatures

**`sendChatHistoryAsync`** — Retrieves messages from a session and sends them:

```typescript
import { McpToolServerConfigurationService, ChatHistoryMessage } from '@microsoft/agents-a365-tooling';
import { TurnContext } from '@microsoft/agents-hosting';

// Build chat history from your conversation tracking
const chatHistory: ChatHistoryMessage[] = [
{
id: 'msg-001',
role: 'user',
content: 'Can you help me find my recent emails?',
timestamp: new Date('2026-01-27T10:00:00Z')
},
{
id: 'msg-002',
role: 'assistant',
content: 'I\'d be happy to help you find your recent emails. Let me search for them now.',
timestamp: new Date('2026-01-27T10:00:05Z')
},
{
id: 'msg-003',
role: 'user',
content: 'Great, show me emails from the last week.',
timestamp: new Date('2026-01-27T10:00:30Z')
}
];
async sendChatHistoryAsync(
turnContext: TurnContext,
sessionId: string,
limit?: number,
toolOptions?: ToolOptions
): Promise<OperationResult>
```

// Send to MCP platform for real-time threat protection
const configService = new McpToolServerConfigurationService();
const result = await configService.sendChatHistory(turnContext, chatHistory);
**`sendChatHistoryMessagesAsync`** — Sends pre-fetched messages:

if (!result.success) {
console.error('Failed to send chat history:', result.error);
}
```typescript
async sendChatHistoryMessagesAsync(
turnContext: TurnContext,
messages: SessionMessage[],
toolOptions?: ToolOptions
): Promise<OperationResult>
```

### Revisit Criteria
### Message Conversion

SessionMessage fields are converted to `ChatHistoryMessage` as follows:

This limitation should be re-evaluated when any of the following occur:
| SessionMessage field | ChatHistoryMessage field | Notes |
|---------------------|------------------------|-------|
| `uuid` | `id` | Falls back to generated UUID if empty |
| `type` | `role` | Normalized: `user`→`user`, `assistant`→`assistant`, `system`→`system` |
| `message.content` | `content` | Extracts text from string content or `text` content blocks; non-text blocks (e.g., `tool_use`, `tool_result`) are ignored |
| *(not available)* | `timestamp` | Generated as current UTC time |

1. **Claude SDK adds history retrieval API** - Monitor for `session.getHistory()`, `session.getMessages()`, or similar methods
2. **`unstable_v2` APIs stabilize** - When APIs lose the `unstable_` prefix, review for new capabilities
3. **SDK version upgrade** - When upgrading `@anthropic-ai/claude-agent-sdk`, check changelog for history-related features
4. **Anthropic documentation updates** - Monitor [TypeScript V2 Preview docs](https://platform.claude.com/docs/en/agent-sdk/typescript-v2-preview) and [GitHub repo](https://github.com/anthropics/claude-agent-sdk-typescript)
Messages with empty extractable content are filtered out.

### Advanced Usage with Pre-fetched Messages

```typescript
import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
import { McpToolRegistrationService } from '@microsoft/agents-a365-tooling-extensions-claude';

// Fetch messages manually for custom processing
const messages = await getSessionMessages(sessionId, { limit: 100 });

// Send to MCP platform
const registrationService = new McpToolRegistrationService();
const result = await registrationService.sendChatHistoryMessagesAsync(turnContext, messages);
```

### References

- [Claude Agent SDK - TypeScript V2 Preview](https://platform.claude.com/docs/en/agent-sdk/typescript-v2-preview)
- [Claude Agent SDK - GitHub Repository](https://github.com/anthropics/claude-agent-sdk-typescript)
- [Claude Agent SDK - npm Package](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk)
- [OpenAI Extension sendChatHistory PR #157](https://github.com/microsoft/Agent365-nodejs/pull/157)
- [Issue #164 - Claude SDK: Monitor for chat history API availability](https://github.com/microsoft/Agent365-nodejs/issues/164)
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
"@microsoft/agents-a365-runtime": "workspace:*",
"@microsoft/agents-a365-tooling": "workspace:*",
"@microsoft/agents-hosting": "catalog:",
"@modelcontextprotocol/sdk": "catalog:"
"@modelcontextprotocol/sdk": "catalog:",
"uuid": "catalog:"
},
"devDependencies": {
"@types/uuid": "catalog:",
"@eslint/js": "catalog:",
"@types/jest": "catalog:",
"@types/node": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { McpToolServerConfigurationService, McpClientTool, Utility, MCPServerConfig, ToolOptions } from '@microsoft/agents-a365-tooling';
import { AgenticAuthenticationService, IConfigurationProvider } from '@microsoft/agents-a365-runtime';
import { v4 as uuidv4 } from 'uuid';
import { McpToolServerConfigurationService, McpClientTool, Utility, MCPServerConfig, ToolOptions, ChatHistoryMessage } from '@microsoft/agents-a365-tooling';
import { AgenticAuthenticationService, OperationResult, OperationError, IConfigurationProvider } from '@microsoft/agents-a365-runtime';
import { ClaudeToolingConfiguration, defaultClaudeToolingConfigurationProvider } from './configuration';

// Agents SDK
import { TurnContext, Authorization } from '@microsoft/agents-hosting';

// Claude SDK expects a different shape for MCP server configs
import type { McpServerConfig, Options } from '@anthropic-ai/claude-agent-sdk';
// Claude SDK
import type { McpServerConfig, Options, SessionMessage, GetSessionMessagesOptions } from '@anthropic-ai/claude-agent-sdk';
import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';

/**
* Represents a content block within a Claude message payload.
*/
interface ContentBlock {
type: string;
text?: string;
}

/**
* Discover MCP servers and list tools formatted for the Claude SDK.
Expand Down Expand Up @@ -96,4 +106,195 @@ export class McpToolRegistrationService {

agentOptions.mcpServers = Object.assign(agentOptions.mcpServers ?? {}, mcpServers);
}

/**
* Sends chat history from a Claude session to the MCP platform for real-time threat protection.
*
* This method retrieves messages from the specified Claude session using `getSessionMessages()`,
* converts them to the `ChatHistoryMessage` format, and sends them to the MCP platform.
*
* @param turnContext - The turn context containing conversation information.
* @param sessionId - The Claude session ID to retrieve messages from.
* @param limit - Optional limit on the number of messages to send. When specified, the most recent N messages are used.
* @param toolOptions - Optional tool options for customization.
* @returns A Promise resolving to an OperationResult indicating success or failure.
* @throws Error if turnContext is null/undefined.
* @throws Error if sessionId is null/undefined/empty.
* @throws Error if required turn context properties are missing.
*
* @example
* ```typescript
* const result = await service.sendChatHistoryAsync(turnContext, 'session-abc-123', 50);
* if (result.succeeded) {
* console.log('Chat history sent successfully');
* } else {
* console.error('Failed to send chat history:', result.errors);
* }
* ```
*/
async sendChatHistoryAsync(
turnContext: TurnContext,
sessionId: string,
limit?: number,
toolOptions?: ToolOptions
): Promise<OperationResult> {
if (!turnContext) {
throw new Error('turnContext is required');
}
if (!sessionId || sessionId.trim().length === 0) {
throw new Error('sessionId is required');
}

let messages: SessionMessage[];
try {
const options: GetSessionMessagesOptions = {};
if (limit !== undefined && limit >= 0) {
options.limit = limit;
}
messages = await getSessionMessages(sessionId, options);
} catch (err: unknown) {
const error = err as Error;
return OperationResult.failed(new OperationError(error));
}

return await this.sendChatHistoryMessagesAsync(
turnContext,
messages,
toolOptions
);
}

/**
* Sends a list of Claude session messages to the MCP platform for real-time threat protection.
*
* This method converts the provided SessionMessage array to `ChatHistoryMessage` format
* and sends them to the MCP platform.
*
* @param turnContext - The turn context containing conversation information.
* @param messages - Array of SessionMessage objects to send.
* @param toolOptions - Optional ToolOptions for customization.
* @returns A Promise resolving to an OperationResult indicating success or failure.
* @throws Error if turnContext is null/undefined.
* @throws Error if messages is null/undefined.
* @throws Error if required turn context properties are missing.
*
* @example
* ```typescript
* import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
* const messages = await getSessionMessages(sessionId);
* const result = await service.sendChatHistoryMessagesAsync(turnContext, messages);
* ```
*/
async sendChatHistoryMessagesAsync(
turnContext: TurnContext,
messages: SessionMessage[],
toolOptions?: ToolOptions
): Promise<OperationResult> {
if (!turnContext) {
throw new Error('turnContext is required');
}
if (!messages) {
throw new Error('messages is required');
}

const effectiveOptions: ToolOptions = {
orchestratorName: toolOptions?.orchestratorName ?? this.orchestratorName
};

let chatHistoryMessages: ChatHistoryMessage[];
try {
chatHistoryMessages = this.convertToChatHistoryMessages(messages);
} catch (err: unknown) {
const error = err as Error;
return OperationResult.failed(new OperationError(error));
}

return await this.configService.sendChatHistory(
turnContext,
chatHistoryMessages,
effectiveOptions
);
}

/**
* Converts Claude SessionMessage array to ChatHistoryMessage format.
* @param messages - Array of SessionMessage objects to convert.
* @returns Array of successfully converted ChatHistoryMessage objects.
*/
private convertToChatHistoryMessages(messages: SessionMessage[]): ChatHistoryMessage[] {
return messages
.map(msg => this.convertSingleMessage(msg))
.filter((msg): msg is ChatHistoryMessage => msg !== null);
}

/**
* Converts a single Claude SessionMessage to ChatHistoryMessage format.
* @param message - The SessionMessage to convert.
* @returns A ChatHistoryMessage object, or null if conversion fails or message has no extractable content.
*/
private convertSingleMessage(message: SessionMessage): ChatHistoryMessage | null {
try {
const content = this.extractContent(message);
if (!content || content.trim().length === 0) {
return null;
}

return {
id: this.extractId(message),
role: message.type === 'assistant' ? 'assistant' : 'user',
Comment thread
gwharris7 marked this conversation as resolved.
Comment thread
gwharris7 marked this conversation as resolved.
content: content,
timestamp: this.extractTimestamp(message)
};
Comment thread
gwharris7 marked this conversation as resolved.
} catch {
return null;
}
}

/**
* Extracts text content from a Claude SessionMessage.
* The message field contains the raw Anthropic API message payload with a `content`
* property that is either a string (user shorthand) or an array of content blocks.
* @param sessionMsg - The SessionMessage to extract content from.
* @returns The extracted content string, or empty string if no text content found.
*/
private extractContent(sessionMsg: SessionMessage): string {
const payload = sessionMsg.message as { content: string | ContentBlock[] };
const { content } = payload;

if (typeof content === 'string') {
return content;
}

if (Array.isArray(content)) {
return content
.filter(block => block.type === 'text' && block.text)
.map(block => block.text!)
.filter(text => text.length > 0)
.join(' ');
}

return '';
}

/**
* Extracts or generates an ID for a message.
* @param message - The SessionMessage to extract or generate an ID for.
* @returns The message UUID, or a newly generated UUID if not present.
*/
private extractId(message: SessionMessage): string {
if (message.uuid) {
return message.uuid;
}
return uuidv4();
}

/**
* Extracts or generates a timestamp for a message.
* Claude SessionMessage does not include timestamps, so current UTC time is used.
* @param _message - The SessionMessage (unused, as timestamps are always generated).
* @returns The current Date.
*/
private extractTimestamp(_message: SessionMessage): Date {
return new Date();
}
}
4 changes: 4 additions & 0 deletions packages/agents-a365-tooling-extensions-claude/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

export * from './McpToolRegistrationService';
export * from './configuration';

// Re-export Claude SDK session utilities for convenience
export { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
export type { SessionMessage, GetSessionMessagesOptions } from '@anthropic-ai/claude-agent-sdk';
Loading
Loading