@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/events.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event mapper: converts almyty StreamEvent / run events into ACP
|
|
3
|
+
* SessionUpdate notifications.
|
|
4
|
+
*
|
|
5
|
+
* ACP SessionUpdate variants: agent_message_chunk, tool_call,
|
|
6
|
+
* tool_call_update, plan, agent_thought_chunk, user_message_chunk.
|
|
7
|
+
*/
|
|
8
|
+
// ── Mapping functions ────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Infer a tool call kind from the node type or tool name.
|
|
11
|
+
*/
|
|
12
|
+
function inferKind(nodeType, toolName) {
|
|
13
|
+
if (nodeType === 'llm_call' || nodeType === 'sub_agent')
|
|
14
|
+
return 'think';
|
|
15
|
+
if (nodeType === 'transform')
|
|
16
|
+
return 'edit';
|
|
17
|
+
if (nodeType === 'condition')
|
|
18
|
+
return 'other';
|
|
19
|
+
if (toolName) {
|
|
20
|
+
const lower = toolName.toLowerCase();
|
|
21
|
+
if (lower.includes('read') || lower.includes('get') || lower.includes('list') || lower.includes('fetch'))
|
|
22
|
+
return 'read';
|
|
23
|
+
if (lower.includes('create') || lower.includes('update') || lower.includes('write') || lower.includes('set'))
|
|
24
|
+
return 'edit';
|
|
25
|
+
if (lower.includes('delete') || lower.includes('remove'))
|
|
26
|
+
return 'delete';
|
|
27
|
+
if (lower.includes('search') || lower.includes('find') || lower.includes('query'))
|
|
28
|
+
return 'search';
|
|
29
|
+
if (lower.includes('execute') || lower.includes('run') || lower.includes('invoke'))
|
|
30
|
+
return 'execute';
|
|
31
|
+
}
|
|
32
|
+
return 'other';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert a single almyty StreamEvent into zero or more ACP SessionUpdate
|
|
36
|
+
* payloads. Most events map 1:1, but some (like node.completed with output)
|
|
37
|
+
* may produce both a tool_call_update and an agent_message_chunk.
|
|
38
|
+
*/
|
|
39
|
+
export function mapStreamEvent(event) {
|
|
40
|
+
const updates = [];
|
|
41
|
+
const data = event.data;
|
|
42
|
+
switch (event.event) {
|
|
43
|
+
// ── Node lifecycle events (workflow agents) ──────────────────
|
|
44
|
+
case 'node.started': {
|
|
45
|
+
const nodeId = String(data.nodeId ?? data.id ?? '');
|
|
46
|
+
const nodeType = String(data.nodeType ?? data.type ?? '');
|
|
47
|
+
const label = String(data.label ?? data.name ?? nodeType);
|
|
48
|
+
updates.push({
|
|
49
|
+
type: 'tool_call',
|
|
50
|
+
toolCallId: nodeId,
|
|
51
|
+
title: label,
|
|
52
|
+
kind: inferKind(nodeType, String(data.toolName ?? '')),
|
|
53
|
+
status: 'running',
|
|
54
|
+
rawInput: data.input ? JSON.stringify(data.input) : undefined,
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'node.output': {
|
|
59
|
+
const nodeId = String(data.nodeId ?? data.id ?? '');
|
|
60
|
+
const output = data.output ?? data.result;
|
|
61
|
+
const outputStr = typeof output === 'string' ? output : JSON.stringify(output);
|
|
62
|
+
// If this is an LLM node, surface as agent message chunk
|
|
63
|
+
if (data.nodeType === 'llm_call' || data.type === 'llm_call') {
|
|
64
|
+
updates.push({
|
|
65
|
+
type: 'agent_message_chunk',
|
|
66
|
+
text: outputStr,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
updates.push({
|
|
70
|
+
type: 'tool_call_update',
|
|
71
|
+
toolCallId: nodeId,
|
|
72
|
+
status: 'completed',
|
|
73
|
+
content: outputStr,
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case 'node.completed': {
|
|
78
|
+
const nodeId = String(data.nodeId ?? data.id ?? '');
|
|
79
|
+
const output = data.output ?? data.result;
|
|
80
|
+
updates.push({
|
|
81
|
+
type: 'tool_call_update',
|
|
82
|
+
toolCallId: nodeId,
|
|
83
|
+
status: 'completed',
|
|
84
|
+
content: output != null ? (typeof output === 'string' ? output : JSON.stringify(output)) : undefined,
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'node.error': {
|
|
89
|
+
const nodeId = String(data.nodeId ?? data.id ?? '');
|
|
90
|
+
updates.push({
|
|
91
|
+
type: 'tool_call_update',
|
|
92
|
+
toolCallId: nodeId,
|
|
93
|
+
status: 'error',
|
|
94
|
+
error: String(data.error ?? data.message ?? 'Unknown error'),
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
// ── Text streaming events ────────────────────────────────────
|
|
99
|
+
case 'text':
|
|
100
|
+
case 'message':
|
|
101
|
+
case 'chunk':
|
|
102
|
+
case 'token': {
|
|
103
|
+
const text = String(data.text ?? data.content ?? data.chunk ?? data.token ?? '');
|
|
104
|
+
if (text) {
|
|
105
|
+
updates.push({
|
|
106
|
+
type: 'agent_message_chunk',
|
|
107
|
+
text,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'thought':
|
|
113
|
+
case 'thinking': {
|
|
114
|
+
const text = String(data.text ?? data.content ?? '');
|
|
115
|
+
if (text) {
|
|
116
|
+
updates.push({
|
|
117
|
+
type: 'agent_thought_chunk',
|
|
118
|
+
text,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
// ── Tool call events (autonomous agents) ─────────────────────
|
|
124
|
+
case 'tool_call':
|
|
125
|
+
case 'tool.started': {
|
|
126
|
+
const toolCallId = String(data.toolCallId ?? data.id ?? data.callId ?? '');
|
|
127
|
+
const toolName = String(data.toolName ?? data.name ?? data.tool ?? '');
|
|
128
|
+
updates.push({
|
|
129
|
+
type: 'tool_call',
|
|
130
|
+
toolCallId,
|
|
131
|
+
title: toolName,
|
|
132
|
+
kind: inferKind(undefined, toolName),
|
|
133
|
+
status: 'running',
|
|
134
|
+
rawInput: data.arguments ? JSON.stringify(data.arguments) : (data.input ? JSON.stringify(data.input) : undefined),
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'tool_result':
|
|
139
|
+
case 'tool.completed': {
|
|
140
|
+
const toolCallId = String(data.toolCallId ?? data.id ?? data.callId ?? '');
|
|
141
|
+
const result = data.result ?? data.output;
|
|
142
|
+
updates.push({
|
|
143
|
+
type: 'tool_call_update',
|
|
144
|
+
toolCallId,
|
|
145
|
+
status: 'completed',
|
|
146
|
+
content: result != null ? (typeof result === 'string' ? result : JSON.stringify(result)) : undefined,
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'tool.error': {
|
|
151
|
+
const toolCallId = String(data.toolCallId ?? data.id ?? data.callId ?? '');
|
|
152
|
+
updates.push({
|
|
153
|
+
type: 'tool_call_update',
|
|
154
|
+
toolCallId,
|
|
155
|
+
status: 'error',
|
|
156
|
+
error: String(data.error ?? data.message ?? 'Tool execution failed'),
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
// ── Execution lifecycle ──────────────────────────────────────
|
|
161
|
+
case 'execution.started':
|
|
162
|
+
case 'execution.completed':
|
|
163
|
+
case 'execution.error':
|
|
164
|
+
case 'run.completed':
|
|
165
|
+
case 'run.error':
|
|
166
|
+
case 'done':
|
|
167
|
+
// Terminal events — the caller handles these to finalize the
|
|
168
|
+
// PromptResponse. We optionally surface errors as message chunks.
|
|
169
|
+
if (data.error) {
|
|
170
|
+
updates.push({
|
|
171
|
+
type: 'agent_message_chunk',
|
|
172
|
+
text: `Error: ${String(data.error)}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (data.output && event.event === 'execution.completed') {
|
|
176
|
+
const text = typeof data.output === 'string' ? data.output : JSON.stringify(data.output);
|
|
177
|
+
updates.push({
|
|
178
|
+
type: 'agent_message_chunk',
|
|
179
|
+
text,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
// Unknown event types are silently ignored.
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
return updates;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Build an ACP Plan from almyty pipeline nodes. Maps node types to
|
|
191
|
+
* priorities and initializes all entries as "todo".
|
|
192
|
+
*/
|
|
193
|
+
export function buildPlanFromPipeline(nodes) {
|
|
194
|
+
const entries = nodes
|
|
195
|
+
.filter((n) => n.type !== 'input' && n.type !== 'output')
|
|
196
|
+
.map((node) => ({
|
|
197
|
+
title: node.label || `${node.type} (${node.id})`,
|
|
198
|
+
priority: inferPriority(node.type),
|
|
199
|
+
status: 'todo',
|
|
200
|
+
}));
|
|
201
|
+
return { type: 'plan', entries };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Map node type to plan entry priority.
|
|
205
|
+
*/
|
|
206
|
+
function inferPriority(nodeType) {
|
|
207
|
+
switch (nodeType) {
|
|
208
|
+
case 'llm_call':
|
|
209
|
+
case 'tool_call':
|
|
210
|
+
case 'sub_agent':
|
|
211
|
+
return 'high';
|
|
212
|
+
case 'condition':
|
|
213
|
+
case 'loop':
|
|
214
|
+
case 'parallel':
|
|
215
|
+
return 'medium';
|
|
216
|
+
default:
|
|
217
|
+
return 'low';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if a StreamEvent indicates the execution has finished.
|
|
222
|
+
*/
|
|
223
|
+
export function isTerminalEvent(event) {
|
|
224
|
+
return [
|
|
225
|
+
'execution.completed',
|
|
226
|
+
'execution.error',
|
|
227
|
+
'run.completed',
|
|
228
|
+
'run.error',
|
|
229
|
+
'done',
|
|
230
|
+
].includes(event.event);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Extract final output text from a terminal StreamEvent.
|
|
234
|
+
*/
|
|
235
|
+
export function extractFinalOutput(event) {
|
|
236
|
+
const data = event.data;
|
|
237
|
+
if (data.output) {
|
|
238
|
+
return typeof data.output === 'string' ? data.output : JSON.stringify(data.output);
|
|
239
|
+
}
|
|
240
|
+
if (data.result) {
|
|
241
|
+
return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
|
|
242
|
+
}
|
|
243
|
+
if (data.error) {
|
|
244
|
+
return `Error: ${String(data.error)}`;
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @almyty/acp-server — ACP agent server for almyty.
|
|
4
|
+
*
|
|
5
|
+
* Exposes any almyty agent via the Agent Client Protocol (ACP) over
|
|
6
|
+
* ndjson stdio. Compatible with Zed, JetBrains AI, and other ACP clients.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @almyty/acp-server <agent> Start ACP server for a specific agent
|
|
10
|
+
* npx @almyty/acp-server my-agent Agent by name or ID
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* ALMYTY_URL Base URL of the almyty backend (default: https://api.almyty.com)
|
|
14
|
+
* ALMYTY_TOKEN API key (auto-read from ~/.almyty/credentials.json)
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @almyty/acp-server — ACP agent server for almyty.
|
|
4
|
+
*
|
|
5
|
+
* Exposes any almyty agent via the Agent Client Protocol (ACP) over
|
|
6
|
+
* ndjson stdio. Compatible with Zed, JetBrains AI, and other ACP clients.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @almyty/acp-server <agent> Start ACP server for a specific agent
|
|
10
|
+
* npx @almyty/acp-server my-agent Agent by name or ID
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* ALMYTY_URL Base URL of the almyty backend (default: https://api.almyty.com)
|
|
14
|
+
* ALMYTY_TOKEN API key (auto-read from ~/.almyty/credentials.json)
|
|
15
|
+
*/
|
|
16
|
+
import * as readline from 'node:readline';
|
|
17
|
+
import { resolveCredentials } from './auth.js';
|
|
18
|
+
import { AlmytyProxy } from './proxy.js';
|
|
19
|
+
import { AlmytyAcpAgent } from './agent.js';
|
|
20
|
+
// ── Argument parsing ─────────────────────────────────────────────
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const HELP_FLAGS = ['--help', '-h'];
|
|
23
|
+
if (args.some((a) => HELP_FLAGS.includes(a)) || args.length === 0) {
|
|
24
|
+
const text = `
|
|
25
|
+
@almyty/acp-server — Expose any almyty agent via the Agent Client Protocol
|
|
26
|
+
|
|
27
|
+
Connects an almyty agent to ACP-compatible clients (Zed, JetBrains AI,
|
|
28
|
+
Toad, and others) over ndjson stdio. The server proxies all requests to
|
|
29
|
+
the almyty backend REST API.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
npx @almyty/acp-server <agent>
|
|
33
|
+
npx @almyty/acp-server my-agent
|
|
34
|
+
npx @almyty/acp-server 550e8400-e29b-41d4-a716-446655440000
|
|
35
|
+
|
|
36
|
+
Authentication:
|
|
37
|
+
npx @almyty/auth login Browser-based login (one-time setup)
|
|
38
|
+
|
|
39
|
+
Environment:
|
|
40
|
+
ALMYTY_URL Base URL (default: https://api.almyty.com)
|
|
41
|
+
ALMYTY_TOKEN API key (auto-read from ~/.almyty/credentials.json)
|
|
42
|
+
|
|
43
|
+
Client configuration:
|
|
44
|
+
|
|
45
|
+
Zed (settings.json):
|
|
46
|
+
{
|
|
47
|
+
"agent": {
|
|
48
|
+
"profiles": {
|
|
49
|
+
"almyty": {
|
|
50
|
+
"provider": "agent-client-protocol",
|
|
51
|
+
"binary": {
|
|
52
|
+
"name": "npx",
|
|
53
|
+
"args": ["-y", "@almyty/acp-server", "my-agent"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
JetBrains (AI Assistant settings):
|
|
61
|
+
Binary path: npx
|
|
62
|
+
Arguments: -y @almyty/acp-server my-agent
|
|
63
|
+
|
|
64
|
+
Custom / stdio:
|
|
65
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}' | npx @almyty/acp-server my-agent
|
|
66
|
+
`;
|
|
67
|
+
// Write help to stdout (not stderr) so piping works
|
|
68
|
+
process.stdout.write(text.trimStart());
|
|
69
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
70
|
+
}
|
|
71
|
+
// Filter out any flags, take the first positional arg as the agent identifier
|
|
72
|
+
const agentArg = args.find((a) => !a.startsWith('-'));
|
|
73
|
+
if (!agentArg) {
|
|
74
|
+
process.stderr.write('Error: Agent name or ID is required.\nUsage: npx @almyty/acp-server <agent>\n');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// ── Main ─────────────────────────────────────────────────────────
|
|
78
|
+
async function main() {
|
|
79
|
+
// Resolve credentials
|
|
80
|
+
const creds = resolveCredentials();
|
|
81
|
+
if (!creds) {
|
|
82
|
+
process.stderr.write('Error: No authentication token found.\n' +
|
|
83
|
+
'Set ALMYTY_TOKEN environment variable or run: npx @almyty/auth login\n');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const proxy = new AlmytyProxy(creds.url, creds.token);
|
|
87
|
+
// Resolve agent: try as ID first, then search by name/slug
|
|
88
|
+
let agentId = agentArg;
|
|
89
|
+
try {
|
|
90
|
+
const agent = await proxy.getAgent(agentId);
|
|
91
|
+
agentId = agent.id;
|
|
92
|
+
process.stderr.write(`[acp] Agent resolved: ${agent.name} (${agent.id}, mode: ${agent.mode})\n`);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// If direct lookup fails, try listing agents and matching by name/slug
|
|
96
|
+
try {
|
|
97
|
+
const agents = await proxy.listAgents();
|
|
98
|
+
const match = agents.find((a) => a.name === agentArg ||
|
|
99
|
+
a.slug === agentArg ||
|
|
100
|
+
a.name.toLowerCase() === agentArg.toLowerCase() ||
|
|
101
|
+
a.slug?.toLowerCase() === agentArg.toLowerCase());
|
|
102
|
+
if (match) {
|
|
103
|
+
agentId = match.id;
|
|
104
|
+
process.stderr.write(`[acp] Agent resolved by name: ${match.name} (${match.id}, mode: ${match.mode})\n`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
process.stderr.write(`Error: Agent "${agentArg}" not found.\n` +
|
|
108
|
+
`Available agents: ${agents.map((a) => a.name).join(', ') || '(none)'}\n`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (listErr) {
|
|
113
|
+
process.stderr.write(`Error: Could not resolve agent "${agentArg}": ${listErr}\n`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Create the send function: write ndjson to stdout
|
|
118
|
+
const send = (msg) => {
|
|
119
|
+
try {
|
|
120
|
+
const line = JSON.stringify(msg);
|
|
121
|
+
process.stdout.write(line + '\n');
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
process.stderr.write(`[acp] Failed to serialize message: ${err}\n`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// Create the ACP agent handler
|
|
128
|
+
const agent = new AlmytyAcpAgent(proxy, agentId, send);
|
|
129
|
+
// Set up ndjson readline on stdin
|
|
130
|
+
const rl = readline.createInterface({
|
|
131
|
+
input: process.stdin,
|
|
132
|
+
terminal: false,
|
|
133
|
+
});
|
|
134
|
+
process.stderr.write(`[acp] Server ready. Listening for JSON-RPC messages on stdin.\n`);
|
|
135
|
+
rl.on('line', async (line) => {
|
|
136
|
+
// Skip empty lines
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
if (!trimmed)
|
|
139
|
+
return;
|
|
140
|
+
// Parse JSON-RPC message
|
|
141
|
+
let msg;
|
|
142
|
+
try {
|
|
143
|
+
msg = JSON.parse(trimmed);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Malformed JSON — send parse error if it looks like it had an id
|
|
147
|
+
send({
|
|
148
|
+
jsonrpc: '2.0',
|
|
149
|
+
id: null,
|
|
150
|
+
error: { code: -32700, message: 'Parse error: invalid JSON' },
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Validate basic JSON-RPC structure
|
|
155
|
+
if (!msg.jsonrpc || msg.jsonrpc !== '2.0' || !msg.method) {
|
|
156
|
+
if (msg.id !== undefined) {
|
|
157
|
+
send({
|
|
158
|
+
jsonrpc: '2.0',
|
|
159
|
+
id: msg.id,
|
|
160
|
+
error: { code: -32600, message: 'Invalid JSON-RPC 2.0 request' },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Dispatch to agent handler
|
|
166
|
+
try {
|
|
167
|
+
await agent.handleMessage(msg);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
process.stderr.write(`[acp] Unhandled error in message handler: ${err}\n`);
|
|
171
|
+
if (msg.id !== undefined && msg.id !== null) {
|
|
172
|
+
send({
|
|
173
|
+
jsonrpc: '2.0',
|
|
174
|
+
id: msg.id,
|
|
175
|
+
error: { code: -32603, message: 'Internal error' },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// Handle stdin close (client disconnected)
|
|
181
|
+
rl.on('close', () => {
|
|
182
|
+
process.stderr.write('[acp] Client disconnected. Shutting down.\n');
|
|
183
|
+
agent.shutdown();
|
|
184
|
+
process.exit(0);
|
|
185
|
+
});
|
|
186
|
+
// Handle process signals
|
|
187
|
+
const shutdown = () => {
|
|
188
|
+
process.stderr.write('[acp] Shutting down.\n');
|
|
189
|
+
agent.shutdown();
|
|
190
|
+
process.exit(0);
|
|
191
|
+
};
|
|
192
|
+
process.on('SIGINT', shutdown);
|
|
193
|
+
process.on('SIGTERM', shutdown);
|
|
194
|
+
// Handle uncaught errors — never crash the stdio process
|
|
195
|
+
process.on('uncaughtException', (err) => {
|
|
196
|
+
process.stderr.write(`[acp] Uncaught exception: ${err}\n`);
|
|
197
|
+
});
|
|
198
|
+
process.on('unhandledRejection', (err) => {
|
|
199
|
+
process.stderr.write(`[acp] Unhandled rejection: ${err}\n`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
main().catch((err) => {
|
|
203
|
+
process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : err}\n`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
/** Minimal agent shape returned by the backend. */
|
|
9
|
+
export interface AgentInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
slug?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
mode: 'workflow' | 'autonomous';
|
|
15
|
+
status?: string;
|
|
16
|
+
pipeline?: {
|
|
17
|
+
nodes?: PipelineNode[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface PipelineNode {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
config?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
/** Autonomous run state from the backend. */
|
|
27
|
+
export interface AgentRun {
|
|
28
|
+
id: string;
|
|
29
|
+
agentId: string;
|
|
30
|
+
status: 'pending' | 'running' | 'waiting_input' | 'completed' | 'failed' | 'cancelled';
|
|
31
|
+
conversationId?: string;
|
|
32
|
+
output?: unknown;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/** A single SSE event emitted by the streaming endpoints. */
|
|
36
|
+
export interface StreamEvent {
|
|
37
|
+
event: string;
|
|
38
|
+
data: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
export declare class AlmytyProxy {
|
|
41
|
+
private readonly baseUrl;
|
|
42
|
+
private readonly token;
|
|
43
|
+
constructor(baseUrl: string, token: string);
|
|
44
|
+
listAgents(): Promise<AgentInfo[]>;
|
|
45
|
+
getAgent(id: string): Promise<AgentInfo>;
|
|
46
|
+
invokeAgent(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Stream a workflow agent execution via SSE. Yields StreamEvent objects
|
|
49
|
+
* as they arrive. The caller is responsible for consuming the async
|
|
50
|
+
* iterator; aborting the signal will terminate the stream.
|
|
51
|
+
*/
|
|
52
|
+
streamAgent(id: string, input: Record<string, unknown>, signal?: AbortSignal): AsyncGenerator<StreamEvent>;
|
|
53
|
+
startRun(id: string, input: Record<string, unknown>): Promise<AgentRun>;
|
|
54
|
+
getRun(agentId: string, runId: string): Promise<AgentRun>;
|
|
55
|
+
sendRunInput(agentId: string, runId: string, input: Record<string, unknown>): Promise<AgentRun>;
|
|
56
|
+
cancelRun(agentId: string, runId: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Stream an autonomous run via SSE. Similar to streamAgent but uses the
|
|
59
|
+
* run-specific SSE endpoint.
|
|
60
|
+
*/
|
|
61
|
+
streamRun(agentId: string, runId: string, signal?: AbortSignal): AsyncGenerator<StreamEvent>;
|
|
62
|
+
private headers;
|
|
63
|
+
private get;
|
|
64
|
+
private post;
|
|
65
|
+
/**
|
|
66
|
+
* Consume an SSE stream from a POST endpoint. Uses eventsource-parser
|
|
67
|
+
* to handle the text/event-stream format.
|
|
68
|
+
*/
|
|
69
|
+
private ssePost;
|
|
70
|
+
/**
|
|
71
|
+
* Consume an SSE stream from a GET endpoint.
|
|
72
|
+
*/
|
|
73
|
+
private sseGet;
|
|
74
|
+
/**
|
|
75
|
+
* Parse an SSE response body into StreamEvent objects.
|
|
76
|
+
*/
|
|
77
|
+
private consumeSse;
|
|
78
|
+
}
|