@almyty/acp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/auth.test.d.ts +1 -0
- package/dist/__tests__/auth.test.js +29 -0
- package/dist/__tests__/events.test.d.ts +1 -0
- package/dist/__tests__/events.test.js +81 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +144 -0
- package/dist/__tests__/session.test.d.ts +1 -0
- package/dist/__tests__/session.test.js +69 -0
- package/dist/agent.d.ts +85 -0
- package/dist/agent.js +527 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +36 -0
- package/dist/events.d.ts +76 -0
- package/dist/events.js +247 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +205 -0
- package/dist/proxy.d.ts +78 -0
- package/dist/proxy.js +209 -0
- package/dist/session.d.ts +56 -0
- package/dist/session.js +77 -0
- package/package.json +39 -0
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP proxy client to the almyty backend REST API.
|
|
3
|
+
*
|
|
4
|
+
* Provides typed methods for agent discovery, invocation, streaming,
|
|
5
|
+
* and autonomous run management. All stdout is reserved for the ACP
|
|
6
|
+
* protocol; diagnostics go to stderr.
|
|
7
|
+
*/
|
|
8
|
+
import { createParser } from 'eventsource-parser';
|
|
9
|
+
/** Default timeouts (ms). */
|
|
10
|
+
const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
11
|
+
const INVOKE_TIMEOUT_MS = 120_000;
|
|
12
|
+
export class AlmytyProxy {
|
|
13
|
+
baseUrl;
|
|
14
|
+
token;
|
|
15
|
+
constructor(baseUrl, token) {
|
|
16
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
17
|
+
this.token = token;
|
|
18
|
+
}
|
|
19
|
+
// ── Agent discovery ────────────────────────────────────────────
|
|
20
|
+
async listAgents() {
|
|
21
|
+
const data = await this.get('/agents', DISCOVERY_TIMEOUT_MS);
|
|
22
|
+
// Backend returns { success, data: [...], pagination } or { agents: [...] }
|
|
23
|
+
const list = Array.isArray(data?.data) ? data.data : (data?.agents || (Array.isArray(data) ? data : []));
|
|
24
|
+
return list;
|
|
25
|
+
}
|
|
26
|
+
async getAgent(id) {
|
|
27
|
+
const data = await this.get(`/agents/${encodeURIComponent(id)}`, DISCOVERY_TIMEOUT_MS);
|
|
28
|
+
return data?.data || data;
|
|
29
|
+
}
|
|
30
|
+
// ── Workflow invocation ────────────────────────────────────────
|
|
31
|
+
async invokeAgent(id, input) {
|
|
32
|
+
return this.post(`/agents/${encodeURIComponent(id)}/invoke`, input, INVOKE_TIMEOUT_MS);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Stream a workflow agent execution via SSE. Yields StreamEvent objects
|
|
36
|
+
* as they arrive. The caller is responsible for consuming the async
|
|
37
|
+
* iterator; aborting the signal will terminate the stream.
|
|
38
|
+
*/
|
|
39
|
+
async *streamAgent(id, input, signal) {
|
|
40
|
+
yield* this.ssePost(`/agents/${encodeURIComponent(id)}/stream`, input, signal);
|
|
41
|
+
}
|
|
42
|
+
// ── Autonomous run management ──────────────────────────────────
|
|
43
|
+
async startRun(id, input) {
|
|
44
|
+
const data = await this.post(`/agents/${encodeURIComponent(id)}/runs`, { input }, INVOKE_TIMEOUT_MS);
|
|
45
|
+
return data?.data || data;
|
|
46
|
+
}
|
|
47
|
+
async getRun(agentId, runId) {
|
|
48
|
+
const data = await this.get(`/agents/${encodeURIComponent(agentId)}/runs/${encodeURIComponent(runId)}`, DISCOVERY_TIMEOUT_MS);
|
|
49
|
+
return data?.data || data;
|
|
50
|
+
}
|
|
51
|
+
async sendRunInput(agentId, runId, input) {
|
|
52
|
+
return this.post(`/agents/${encodeURIComponent(agentId)}/runs/${encodeURIComponent(runId)}/input`, input, INVOKE_TIMEOUT_MS);
|
|
53
|
+
}
|
|
54
|
+
async cancelRun(agentId, runId) {
|
|
55
|
+
await this.post(`/agents/${encodeURIComponent(agentId)}/runs/${encodeURIComponent(runId)}/cancel`, {}, DISCOVERY_TIMEOUT_MS);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Stream an autonomous run via SSE. Similar to streamAgent but uses the
|
|
59
|
+
* run-specific SSE endpoint.
|
|
60
|
+
*/
|
|
61
|
+
async *streamRun(agentId, runId, signal) {
|
|
62
|
+
yield* this.sseGet(`/agents/${encodeURIComponent(agentId)}/runs/${encodeURIComponent(runId)}/stream`, signal);
|
|
63
|
+
}
|
|
64
|
+
// ── Internal HTTP helpers ──────────────────────────────────────
|
|
65
|
+
headers() {
|
|
66
|
+
return {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
Authorization: `Bearer ${this.token}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async get(path, timeoutMs) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
76
|
+
method: 'GET',
|
|
77
|
+
headers: this.headers(),
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
if (!resp.ok) {
|
|
81
|
+
const text = await resp.text().catch(() => '');
|
|
82
|
+
throw new Error(`GET ${path} failed (${resp.status}): ${text}`);
|
|
83
|
+
}
|
|
84
|
+
return (await resp.json());
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
88
|
+
throw new Error(`GET ${path} timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async post(path, body, timeoutMs) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
99
|
+
try {
|
|
100
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: this.headers(),
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
});
|
|
106
|
+
if (!resp.ok) {
|
|
107
|
+
const text = await resp.text().catch(() => '');
|
|
108
|
+
throw new Error(`POST ${path} failed (${resp.status}): ${text}`);
|
|
109
|
+
}
|
|
110
|
+
return (await resp.json());
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
114
|
+
throw new Error(`POST ${path} timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Consume an SSE stream from a POST endpoint. Uses eventsource-parser
|
|
124
|
+
* to handle the text/event-stream format.
|
|
125
|
+
*/
|
|
126
|
+
async *ssePost(path, body, signal) {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const combinedSignal = signal
|
|
129
|
+
? AbortSignal.any([signal, controller.signal])
|
|
130
|
+
: controller.signal;
|
|
131
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
...this.headers(),
|
|
135
|
+
Accept: 'text/event-stream',
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(body),
|
|
138
|
+
signal: combinedSignal,
|
|
139
|
+
});
|
|
140
|
+
if (!resp.ok) {
|
|
141
|
+
const text = await resp.text().catch(() => '');
|
|
142
|
+
throw new Error(`SSE POST ${path} failed (${resp.status}): ${text}`);
|
|
143
|
+
}
|
|
144
|
+
yield* this.consumeSse(resp);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Consume an SSE stream from a GET endpoint.
|
|
148
|
+
*/
|
|
149
|
+
async *sseGet(path, signal) {
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const combinedSignal = signal
|
|
152
|
+
? AbortSignal.any([signal, controller.signal])
|
|
153
|
+
: controller.signal;
|
|
154
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
155
|
+
method: 'GET',
|
|
156
|
+
headers: {
|
|
157
|
+
...this.headers(),
|
|
158
|
+
Accept: 'text/event-stream',
|
|
159
|
+
},
|
|
160
|
+
signal: combinedSignal,
|
|
161
|
+
});
|
|
162
|
+
if (!resp.ok) {
|
|
163
|
+
const text = await resp.text().catch(() => '');
|
|
164
|
+
throw new Error(`SSE GET ${path} failed (${resp.status}): ${text}`);
|
|
165
|
+
}
|
|
166
|
+
yield* this.consumeSse(resp);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse an SSE response body into StreamEvent objects.
|
|
170
|
+
*/
|
|
171
|
+
async *consumeSse(resp) {
|
|
172
|
+
const body = resp.body;
|
|
173
|
+
if (!body)
|
|
174
|
+
return;
|
|
175
|
+
// Buffer for events parsed by eventsource-parser
|
|
176
|
+
const events = [];
|
|
177
|
+
let done = false;
|
|
178
|
+
const parser = createParser({
|
|
179
|
+
onEvent(event) {
|
|
180
|
+
let data;
|
|
181
|
+
try {
|
|
182
|
+
data = JSON.parse(event.data);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
data = { raw: event.data };
|
|
186
|
+
}
|
|
187
|
+
events.push({ event: event.event || 'message', data });
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const reader = body.getReader();
|
|
191
|
+
const decoder = new TextDecoder();
|
|
192
|
+
try {
|
|
193
|
+
while (!done) {
|
|
194
|
+
const { value, done: readerDone } = await reader.read();
|
|
195
|
+
done = readerDone;
|
|
196
|
+
if (value) {
|
|
197
|
+
parser.feed(decoder.decode(value, { stream: !done }));
|
|
198
|
+
}
|
|
199
|
+
// Yield any buffered events
|
|
200
|
+
while (events.length > 0) {
|
|
201
|
+
yield events.shift();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
reader.releaseLock();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager for the ACP server.
|
|
3
|
+
*
|
|
4
|
+
* Maps ACP session IDs to almyty agent state: agent ID, agent mode
|
|
5
|
+
* (workflow/autonomous), conversation context, and active run tracking.
|
|
6
|
+
*/
|
|
7
|
+
/** Persisted state for one ACP session. */
|
|
8
|
+
export interface Session {
|
|
9
|
+
/** ACP session ID (UUID). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** almyty agent ID this session is bound to. */
|
|
12
|
+
agentId: string;
|
|
13
|
+
/** Agent execution mode. */
|
|
14
|
+
mode: 'workflow' | 'autonomous';
|
|
15
|
+
/** Conversation ID for autonomous multi-turn sessions. */
|
|
16
|
+
conversationId?: string;
|
|
17
|
+
/** Currently in-flight run ID (for cancellation). */
|
|
18
|
+
activeRunId?: string;
|
|
19
|
+
/** AbortController for the active streaming request. */
|
|
20
|
+
abortController?: AbortController;
|
|
21
|
+
/** Timestamp of last activity (epoch ms). */
|
|
22
|
+
lastActivity: number;
|
|
23
|
+
/** User-requested session mode (e.g. "plan", "code"). Stored but not enforced. */
|
|
24
|
+
userMode?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class SessionManager {
|
|
27
|
+
private readonly sessions;
|
|
28
|
+
/**
|
|
29
|
+
* Create a new session bound to the given agent.
|
|
30
|
+
*/
|
|
31
|
+
create(agentId: string, mode: 'workflow' | 'autonomous'): Session;
|
|
32
|
+
/**
|
|
33
|
+
* Look up a session by ID. Returns undefined if not found.
|
|
34
|
+
*/
|
|
35
|
+
get(sessionId: string): Session | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* List all active sessions.
|
|
38
|
+
*/
|
|
39
|
+
list(): Session[];
|
|
40
|
+
/**
|
|
41
|
+
* Update fields on an existing session.
|
|
42
|
+
*/
|
|
43
|
+
update(sessionId: string, patch: Partial<Pick<Session, 'conversationId' | 'activeRunId' | 'abortController' | 'userMode'>>): void;
|
|
44
|
+
/**
|
|
45
|
+
* Abort any in-flight work for a session and clear the active run.
|
|
46
|
+
*/
|
|
47
|
+
cancel(sessionId: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Close and remove a session, aborting any in-flight work.
|
|
50
|
+
*/
|
|
51
|
+
close(sessionId: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Close all sessions. Called during shutdown.
|
|
54
|
+
*/
|
|
55
|
+
closeAll(): void;
|
|
56
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager for the ACP server.
|
|
3
|
+
*
|
|
4
|
+
* Maps ACP session IDs to almyty agent state: agent ID, agent mode
|
|
5
|
+
* (workflow/autonomous), conversation context, and active run tracking.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
export class SessionManager {
|
|
9
|
+
sessions = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Create a new session bound to the given agent.
|
|
12
|
+
*/
|
|
13
|
+
create(agentId, mode) {
|
|
14
|
+
const session = {
|
|
15
|
+
id: randomUUID(),
|
|
16
|
+
agentId,
|
|
17
|
+
mode,
|
|
18
|
+
lastActivity: Date.now(),
|
|
19
|
+
};
|
|
20
|
+
this.sessions.set(session.id, session);
|
|
21
|
+
return session;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Look up a session by ID. Returns undefined if not found.
|
|
25
|
+
*/
|
|
26
|
+
get(sessionId) {
|
|
27
|
+
return this.sessions.get(sessionId);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* List all active sessions.
|
|
31
|
+
*/
|
|
32
|
+
list() {
|
|
33
|
+
return Array.from(this.sessions.values());
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Update fields on an existing session.
|
|
37
|
+
*/
|
|
38
|
+
update(sessionId, patch) {
|
|
39
|
+
const session = this.sessions.get(sessionId);
|
|
40
|
+
if (!session)
|
|
41
|
+
return;
|
|
42
|
+
Object.assign(session, patch);
|
|
43
|
+
session.lastActivity = Date.now();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Abort any in-flight work for a session and clear the active run.
|
|
47
|
+
*/
|
|
48
|
+
cancel(sessionId) {
|
|
49
|
+
const session = this.sessions.get(sessionId);
|
|
50
|
+
if (!session)
|
|
51
|
+
return;
|
|
52
|
+
session.abortController?.abort();
|
|
53
|
+
session.abortController = undefined;
|
|
54
|
+
session.activeRunId = undefined;
|
|
55
|
+
session.lastActivity = Date.now();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Close and remove a session, aborting any in-flight work.
|
|
59
|
+
*/
|
|
60
|
+
close(sessionId) {
|
|
61
|
+
const session = this.sessions.get(sessionId);
|
|
62
|
+
if (!session)
|
|
63
|
+
return false;
|
|
64
|
+
session.abortController?.abort();
|
|
65
|
+
this.sessions.delete(sessionId);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Close all sessions. Called during shutdown.
|
|
70
|
+
*/
|
|
71
|
+
closeAll() {
|
|
72
|
+
for (const session of this.sessions.values()) {
|
|
73
|
+
session.abortController?.abort();
|
|
74
|
+
}
|
|
75
|
+
this.sessions.clear();
|
|
76
|
+
}
|
|
77
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@almyty/acp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ACP agent server for almyty — expose any almyty agent via the Agent Client Protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"almyty-acp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"acp",
|
|
22
|
+
"agent-client-protocol",
|
|
23
|
+
"ai-agents",
|
|
24
|
+
"almyty",
|
|
25
|
+
"zed",
|
|
26
|
+
"jetbrains"
|
|
27
|
+
],
|
|
28
|
+
"author": "almyty",
|
|
29
|
+
"license": "BSL-1.1",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"eventsource-parser": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"vitest": "^4.1.0"
|
|
38
|
+
}
|
|
39
|
+
}
|