From 2cf7b779de7759d17dc3d1aa18e7c4c6472e96fd Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 20 May 2026 14:53:08 +0000 Subject: [PATCH 1/3] feat: add interruptConversation method to ConversationClient Add interruptConversation() to ConversationClient which calls POST /api/conversations/{id}/interrupt. Unlike pauseConversation which waits for the current LLM call to finish, interrupt cancels the in-flight request so the effect is immediate. The conversation transitions to paused and can be resumed later. This aligns with the new interrupt endpoint added in software-agent-sdk feat/async-llm-completions branch. Co-authored-by: openhands --- src/client/conversation-client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/conversation-client.ts b/src/client/conversation-client.ts index 98ebfcb..e3ea0a7 100644 --- a/src/client/conversation-client.ts +++ b/src/client/conversation-client.ts @@ -90,6 +90,14 @@ export class ConversationClient { return response.data; } + async interruptConversation(conversationId: string): Promise { + const response = await this.client.post( + `/api/conversations/${conversationId}/interrupt`, + {} + ); + return response.data; + } + async runConversation(conversationId: string): Promise { const response = await this.client.post( `/api/conversations/${conversationId}/run`, From 6edcadbedc89c342f4f23ac972287e7b4cb84a20 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 21 May 2026 01:07:05 +0000 Subject: [PATCH 2/3] test: add interruptConversation coverage + bump server to v1.23.0 - Add interruptConversation call and assertion to ConversationClient unit test (addresses review feedback on missing test coverage) - Update integration test server image to v1.23.0 (b1235d0-python) which includes the /interrupt endpoint - Update AGENTS.md to reflect new server version Co-authored-by: openhands --- .github/workflows/integration-tests.yml | 2 +- AGENTS.md | 6 +++--- src/__tests__/api-clients.test.ts | 12 +++++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1525f8f..4942d0a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -16,7 +16,7 @@ env: AGENT_SERVER_PORT: 8010 HOST_WORKSPACE_DIR: /tmp/agent-workspace AGENT_WORKSPACE_DIR: /workspace - AGENT_SERVER_IMAGE: ghcr.io/openhands/agent-server:7c37803-python # software-agent-sdk v1.18.1 + AGENT_SERVER_IMAGE: ghcr.io/openhands/agent-server:b1235d0-python # software-agent-sdk v1.23.0 jobs: integration-test: diff --git a/AGENTS.md b/AGENTS.md index 011018a..407a38d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,10 +304,10 @@ Integration tests are in `src/__tests__/integration/` and require a running agen export LLM_API_KEY="your-api-key" export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" -# Start agent-server in Docker (software-agent-sdk v1.18.1) +# Start agent-server in Docker (software-agent-sdk v1.23.0) docker run -d --name agent-server -p 8010:8000 \ -v /tmp/agent-workspace:/workspace \ - ghcr.io/openhands/agent-server:7c37803-python + ghcr.io/openhands/agent-server:b1235d0-python # Run integration tests npm run test:integration @@ -336,7 +336,7 @@ Required GitHub secrets: ### CI Image Version -- The integration workflow pins `ghcr.io/openhands/agent-server:7c37803-python`, which corresponds to the `software-agent-sdk` release `v1.18.1`. +- The integration workflow pins `ghcr.io/openhands/agent-server:b1235d0-python`, which corresponds to the `software-agent-sdk` release `v1.23.0`. - Keep the TypeScript client tests strict against that released server image rather than adding compatibility fallbacks for older prerelease builds. ## Agent Behavior Guidelines diff --git a/src/__tests__/api-clients.test.ts b/src/__tests__/api-clients.test.ts index 7b040ef..edf5efb 100644 --- a/src/__tests__/api-clients.test.ts +++ b/src/__tests__/api-clients.test.ts @@ -760,6 +760,7 @@ describe('Auxiliary API clients', () => { const client = new ConversationClient({ host: 'http://example.com' }); await client.sendEvent('c1', { role: 'user', content: [] }, { run: true }); await client.pauseConversation('c1'); + await client.interruptConversation('c1'); await client.runConversation('c1'); await client.askAgent('c1', 'status?'); await client.respondToConfirmation('c1', { accept: true }); @@ -775,17 +776,22 @@ describe('Auxiliary API clients', () => { }) ); expect(global.fetch).toHaveBeenNthCalledWith( - 4, + 3, + 'http://example.com/api/conversations/c1/interrupt', + expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) + ); + expect(global.fetch).toHaveBeenNthCalledWith( + 5, 'http://example.com/api/conversations/c1/ask_agent', expect.objectContaining({ method: 'POST', body: JSON.stringify({ question: 'status?' }) }) ); expect(global.fetch).toHaveBeenNthCalledWith( - 5, + 6, 'http://example.com/api/conversations/c1/events/respond_to_confirmation', expect.objectContaining({ method: 'POST', body: JSON.stringify({ accept: true }) }) ); expect(global.fetch).toHaveBeenNthCalledWith( - 7, + 8, 'http://example.com/api/conversations/c1', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ title: 'New title' }) }) ); From 56ba50f67cb31f011db1e8ff0c845ea5b8d7dfc5 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 21 May 2026 01:11:52 +0000 Subject: [PATCH 3/3] fix: update integration tests for agent-server v1.23.0 - Add HealthStatus interface: /health now returns {status: 'ok'} instead of plain 'OK' string - Update ServerClient.getHealth() return type to HealthStatus - Export HealthStatus from package index - Fix trajectory download test: v1.23.0 serves real conversation ZIP archives, so verify ZIP magic bytes instead of a manually created file Co-authored-by: openhands --- .../deterministic-api.integration.test.ts | 13 ++++++------- src/client/server-client.ts | 6 +++--- src/index.ts | 1 + src/models/api.ts | 4 ++++ 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/__tests__/integration/deterministic-api.integration.test.ts b/src/__tests__/integration/deterministic-api.integration.test.ts index 315dcf4..c15a5cf 100644 --- a/src/__tests__/integration/deterministic-api.integration.test.ts +++ b/src/__tests__/integration/deterministic-api.integration.test.ts @@ -58,7 +58,7 @@ describe('Deterministic API Integration Tests', () => { expect(root).toBeDefined(); expect(alive.status).toBe('ok'); - expect(health).toBe('OK'); + expect(health.status).toBe('ok'); expect(['ready', 'initializing']).toContain(ready.status); expect(info.version).toBeDefined(); expect(Array.isArray(providers)).toBe(true); @@ -114,14 +114,13 @@ describe('Deterministic API Integration Tests', () => { const finalResponse = await conversation.getAgentFinalResponse(); expect(typeof finalResponse).toBe('string'); - const trajectoryFile = `/workspace/conversations/${conversation.id.replace(/-/g, '')}.zip`; - await workspace.executeCommand( - `mkdir -p /workspace/conversations && printf 'trajectory-data' > ${trajectoryFile}` - ); const trajectory = await conversation.downloadTrajectory(); expect(trajectory).toBeInstanceOf(Blob); - expect(await trajectory.text()).toContain('trajectory-data'); - await workspace.executeCommand(`rm -f ${trajectoryFile}`); + expect(trajectory.size).toBeGreaterThan(0); + const trajectoryBytes = new Uint8Array(await trajectory.arrayBuffer()); + // Verify it's a valid ZIP file (PK\x03\x04 magic bytes) + expect(trajectoryBytes[0]).toBe(0x50); // P + expect(trajectoryBytes[1]).toBe(0x4b); // K const forkedConversation = await conversation.fork({ title: 'Forked from deterministic test', diff --git a/src/client/server-client.ts b/src/client/server-client.ts index f2f202c..1fc8fdb 100644 --- a/src/client/server-client.ts +++ b/src/client/server-client.ts @@ -1,5 +1,5 @@ import { HttpClient } from './http-client'; -import { AliveStatus, ReadyStatus } from '../models/api'; +import { AliveStatus, HealthStatus, ReadyStatus } from '../models/api'; import { ServerInfo } from '../types/base'; export interface ServerClientOptions { @@ -33,8 +33,8 @@ export class ServerClient { return response.data; } - async getHealth(): Promise { - const response = await this.client.get('/health'); + async getHealth(): Promise { + const response = await this.client.get('/health'); return response.data; } diff --git a/src/index.ts b/src/index.ts index cbb896c..a7a91f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -236,6 +236,7 @@ export type { HttpClientOptions, RequestOptions, HttpResponse } from './client/h export type { AliveStatus, + HealthStatus, ReadyStatus, ProvidersResponse, ModelsResponse, diff --git a/src/models/api.ts b/src/models/api.ts index 80331ab..7777fbf 100644 --- a/src/models/api.ts +++ b/src/models/api.ts @@ -8,6 +8,10 @@ export interface AliveStatus { status: string; } +export interface HealthStatus { + status: string; +} + export interface ReadyStatus { status: string; message?: string;