Skip to content

Commit 900d576

Browse files
committed
feat(acp-client): stream to tmp
Enhanced the ACP client test to create sessions, stream NDJSON frames, and persist the output under /tmp/headless-coder-sdk. The server-side session store now uses a global singleton so session state survives across route handlers. Verified end-to-end via npm run acp:e2e.
1 parent cc5104f commit 900d576

File tree

2 files changed

+85
-12
lines changed

2 files changed

+85
-12
lines changed

packages/acp-server/client/src/test.ts

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,90 @@
1+
import { mkdir, open, stat } from 'node:fs/promises';
2+
import path from 'node:path';
3+
14
const BASE_URL = process.env.ACP_BASE_URL ?? 'http://localhost:8000';
25
const token = process.env.ACP_TOKEN;
6+
const STREAM_DIR = '/tmp/headless-coder-sdk';
37

48
function buildHeaders(): HeadersInit {
5-
if (!token) return {};
6-
return { Authorization: `Bearer ${token}` };
9+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
10+
if (token) headers.Authorization = `Bearer ${token}`;
11+
return headers;
12+
}
13+
14+
function assert(condition: unknown, message: string): asserts condition {
15+
if (!condition) {
16+
throw new Error(message);
17+
}
718
}
819

9-
async function assert(cond: unknown, message: string): Promise<void> {
10-
if (!cond) throw new Error(message);
20+
async function listAgents() {
21+
const res = await fetch(`${BASE_URL}/api/acp/agents`, { headers: buildHeaders() });
22+
assert(res.ok, `Failed to fetch agents: ${res.status}`);
23+
const payload = (await res.json()) as { agents: Array<{ id: string }> };
24+
assert(payload.agents.length > 0, 'No agents returned');
25+
console.log(`ACP client: discovered ${payload.agents.length} agent(s).`);
26+
return payload.agents[0].id;
1127
}
1228

13-
async function testAgentsEndpoint(): Promise<void> {
14-
const res = await fetch(`${BASE_URL}/api/acp/agents`, {
29+
async function createSession(provider: string) {
30+
const res = await fetch(`${BASE_URL}/api/acp/sessions`, {
31+
method: 'POST',
1532
headers: buildHeaders(),
33+
body: JSON.stringify({ provider }),
1634
});
17-
await assert(res.ok, `Failed to fetch agents: ${res.status}`);
18-
const data = (await res.json()) as { agents: Array<{ id: string }> };
19-
await assert(Array.isArray(data.agents) && data.agents.length > 0, 'No agents returned');
20-
console.log(`ACP client: discovered ${data.agents.length} agent(s).`);
35+
assert(res.ok, `Failed to create session: ${res.status}`);
36+
const body = (await res.json()) as { sessionId: string };
37+
assert(body.sessionId, 'Missing sessionId');
38+
return body.sessionId;
39+
}
40+
41+
async function streamMessage(sessionId: string): Promise<string> {
42+
await mkdir(STREAM_DIR, { recursive: true });
43+
const outPath = path.join(STREAM_DIR, `stream-${Date.now()}.ndjson`);
44+
const schema = {
45+
type: 'object',
46+
properties: {
47+
summary: { type: 'string' },
48+
risks: { type: 'array', items: { type: 'string' }, minItems: 1 },
49+
},
50+
required: ['summary', 'risks'],
51+
} as const;
52+
53+
const response = await fetch(`${BASE_URL}/api/acp/messages?stream=true`, {
54+
method: 'POST',
55+
headers: buildHeaders(),
56+
body: JSON.stringify({
57+
sessionId,
58+
content: 'Review the repository and return a JSON summary plus key risks.',
59+
outputSchema: schema,
60+
}),
61+
});
62+
assert(response.ok && response.body, `Streaming request failed: ${response.status}`);
63+
64+
const reader = response.body.getReader();
65+
const decoder = new TextDecoder();
66+
const file = await open(outPath, 'w');
67+
68+
try {
69+
while (true) {
70+
const { done, value } = await reader.read();
71+
if (done) break;
72+
await file.write(decoder.decode(value));
73+
}
74+
} finally {
75+
await file.close();
76+
}
77+
78+
const info = await stat(outPath);
79+
assert(info.size > 0, 'Stream file is empty');
80+
console.log(`ACP client: streamed frames saved to ${outPath}`);
81+
return outPath;
2182
}
2283

2384
async function main(): Promise<void> {
24-
await testAgentsEndpoint();
85+
const provider = await listAgents();
86+
const sessionId = await createSession(provider);
87+
await streamMessage(sessionId);
2588
console.log('ACP client tests passed');
2689
}
2790

packages/acp-server/src/acp/store.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@ class SessionStore {
2525
}
2626
}
2727

28-
export const sessions = new SessionStore();
28+
const GLOBAL_KEY = Symbol.for('acp.sessionStore');
29+
30+
function getGlobalStore(): SessionStore {
31+
const globalAny = globalThis as { [GLOBAL_KEY]?: SessionStore };
32+
if (!globalAny[GLOBAL_KEY]) {
33+
globalAny[GLOBAL_KEY] = new SessionStore();
34+
}
35+
return globalAny[GLOBAL_KEY]!;
36+
}
37+
38+
export const sessions = getGlobalStore();
2939

3040
export function buildSessionId(provider: ProviderId, threadId?: string): string {
3141
const suffix = threadId ?? randomUUID();

0 commit comments

Comments
 (0)