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
58 changes: 58 additions & 0 deletions src/__tests__/api-clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,64 @@ describe('Auxiliary API clients', () => {
);
});

it('RemoteConversation.start sends the optional observability user ID', async () => {
global.fetch = jest.fn().mockResolvedValue(
new Response(JSON.stringify({ id: 'conv-123' }), {
status: 200,
headers: { 'content-type': 'application/json' },
})
) as typeof fetch;

const agent = new Agent({ llm: { model: 'gpt-4o', api_key: 'k' } });
const workspace = new RemoteWorkspace({ host: 'http://example.com', workingDir: '/tmp' });
const conversation = new RemoteConversation(agent, workspace, {
userId: 'user-42',
});

await conversation.start({ initialMessage: 'hello' });

expect(global.fetch).toHaveBeenCalledWith(
'http://example.com/api/conversations',
expect.objectContaining({
method: 'POST',
})
);
const [, init] = (global.fetch as jest.Mock).mock.calls[0];
expect(JSON.parse(init.body)).toMatchObject({
user_id: 'user-42',
initial_message: {
role: 'user',
content: [{ type: 'text', text: 'hello' }],
},
});
});

it('ConversationManager.createACPConversation sends the optional observability user ID', async () => {
global.fetch = jest.fn().mockResolvedValue(
new Response(JSON.stringify({ id: 'acp-123' }), {
status: 200,
headers: { 'content-type': 'application/json' },
})
) as typeof fetch;

const manager = new ConversationManager({ host: 'http://example.com' });
await manager.createACPConversation(
{ kind: 'ACPAgent', llm: { model: 'gpt-4o' } },
{ userId: 'user-42' }
);

expect(global.fetch).toHaveBeenCalledWith(
'http://example.com/api/acp/conversations',
expect.objectContaining({
method: 'POST',
})
);
const [, init] = (global.fetch as jest.Mock).mock.calls[0];
expect(JSON.parse(init.body)).toMatchObject({
user_id: 'user-42',
});
});

it('HttpClient can parse blob responses when requested', async () => {
global.fetch = jest.fn().mockResolvedValue(
new Response(new Blob(['zip-data']), {
Expand Down
5 changes: 5 additions & 0 deletions src/conversation/conversation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export class ConversationManager {
maxIterations?: number;
stuckDetection?: boolean;
workingDir?: string;
userId?: string;
} = {}
): Promise<RemoteConversation> {
const workspace = new RemoteWorkspace({
Expand All @@ -226,10 +227,12 @@ export class ConversationManager {
const conversation = new RemoteConversation(agent, workspace, {
maxIterations: options.maxIterations,
stuckDetection: options.stuckDetection,
userId: options.userId,
});

await conversation.start({
initialMessage: options.initialMessage,
userId: options.userId,
});

return conversation;
Expand Down Expand Up @@ -340,6 +343,7 @@ export class ConversationManager {
maxIterations?: number;
stuckDetection?: boolean;
workingDir?: string;
userId?: string;
} = {}
): Promise<ACPConversationInfo> {
let initialMessage: CreateACPConversationRequest['initial_message'];
Expand All @@ -356,6 +360,7 @@ export class ConversationManager {
max_iterations: options.maxIterations || 500,
stuck_detection: options.stuckDetection ?? true,
workspace: { type: 'local', working_dir: options.workingDir || '/tmp' },
user_id: options.userId ?? null,
};

const response = await this.client.post<ACPConversationInfo>('/api/acp/conversations', request);
Expand Down
9 changes: 9 additions & 0 deletions src/conversation/remote-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import type { HookConfig } from '../hooks';
* Options for creating a RemoteConversation instance.
*/
export interface RemoteConversationOptions extends BaseConversationOptions {
/**
* Optional user ID to associate with server-side observability traces.
*/
userId?: string;
/**
* Optional hook configuration for this conversation.
* Hooks are shell scripts that run server-side at key lifecycle events
Expand Down Expand Up @@ -87,6 +91,7 @@ export class RemoteConversation implements IConversation {
private callback?: ConversationCallbackType;
private onError?: ErrorCallbackType;
private hookConfig?: HookConfig;
private userId?: string;

constructor(
agent: AgentBase,
Expand All @@ -99,6 +104,7 @@ export class RemoteConversation implements IConversation {
this.onError = options.onError;
this._conversationId = options.conversationId;
this.hookConfig = options.hookConfig;
this.userId = options.userId;

this.client = new HttpClient({
baseUrl: workspace.host,
Expand Down Expand Up @@ -132,6 +138,7 @@ export class RemoteConversation implements IConversation {
maxIterations?: number;
stuckDetection?: boolean;
hookConfig?: HookConfig;
userId?: string;
} = {}
): Promise<void> {
if (this._conversationId) {
Expand All @@ -151,6 +158,7 @@ export class RemoteConversation implements IConversation {

// Use hook config from start options, falling back to constructor option
const hookConfig = options.hookConfig ?? this.hookConfig ?? undefined;
const userId = options.userId ?? this.userId ?? undefined;

const request: CreateConversationRequest = {
agent: this.agent,
Expand All @@ -159,6 +167,7 @@ export class RemoteConversation implements IConversation {
stuck_detection: options.stuckDetection ?? true,
workspace: { type: 'local', working_dir: this.workspace.workingDir },
hook_config: hookConfig ?? null,
user_id: userId ?? null,
};

const response = await this.client.post<ConversationInfo>('/api/conversations', request);
Expand Down
2 changes: 2 additions & 0 deletions src/models/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface CreateConversationRequest {
stuck_detection: boolean;
workspace: Record<string, unknown>;
hook_config?: HookConfig | null;
user_id?: string | null;
}

export interface CreateACPConversationRequest {
Expand All @@ -98,6 +99,7 @@ export interface CreateACPConversationRequest {
stuck_detection: boolean;
workspace: Record<string, unknown>;
hook_config?: HookConfig | null;
user_id?: string | null;
}

export interface GenerateTitleRequest {
Expand Down
Loading