@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/agent.js ADDED
@@ -0,0 +1,527 @@
1
+ /**
2
+ * AlmytyAcpAgent: handles all ACP JSON-RPC methods.
3
+ *
4
+ * Bridges the ACP protocol to the almyty backend REST API. Manages
5
+ * session lifecycle, dispatches prompts to the correct execution engine,
6
+ * and streams SessionUpdate notifications back to the client.
7
+ */
8
+ import { SessionManager } from './session.js';
9
+ import { mapStreamEvent, buildPlanFromPipeline, isTerminalEvent, extractFinalOutput, } from './events.js';
10
+ // ── Standard JSON-RPC error codes ────────────────────────────────
11
+ const PARSE_ERROR = -32700;
12
+ const INVALID_REQUEST = -32600;
13
+ const METHOD_NOT_FOUND = -32601;
14
+ const INVALID_PARAMS = -32602;
15
+ const INTERNAL_ERROR = -32603;
16
+ // ── Agent implementation ─────────────────────────────────────────
17
+ export class AlmytyAcpAgent {
18
+ proxy;
19
+ sessions;
20
+ agentId;
21
+ send;
22
+ agentInfo = null;
23
+ authenticated = false;
24
+ constructor(proxy, agentId, send) {
25
+ this.proxy = proxy;
26
+ this.sessions = new SessionManager();
27
+ this.agentId = agentId;
28
+ this.send = send;
29
+ }
30
+ /**
31
+ * Update the send function (used if transport is re-initialized).
32
+ */
33
+ setSend(send) {
34
+ this.send = send;
35
+ }
36
+ /**
37
+ * Handle an incoming JSON-RPC message from the ACP client.
38
+ */
39
+ async handleMessage(msg) {
40
+ const { id, method, params } = msg;
41
+ // Notifications have no id — they don't expect a response
42
+ const isNotification = id === undefined || id === null;
43
+ try {
44
+ let result;
45
+ switch (method) {
46
+ case 'initialize':
47
+ result = await this.handleInitialize(params ?? {});
48
+ break;
49
+ case 'authenticate':
50
+ result = await this.handleAuthenticate(params ?? {});
51
+ break;
52
+ case 'session/new':
53
+ result = await this.handleSessionNew(params ?? {});
54
+ break;
55
+ case 'session/list':
56
+ result = await this.handleSessionList();
57
+ break;
58
+ case 'session/load':
59
+ result = await this.handleSessionLoad(params ?? {});
60
+ break;
61
+ case 'session/prompt':
62
+ result = await this.handleSessionPrompt(params ?? {});
63
+ break;
64
+ case 'session/cancel':
65
+ this.handleSessionCancel(params ?? {});
66
+ if (isNotification)
67
+ return; // No response for notifications
68
+ result = {};
69
+ break;
70
+ case 'session/set_mode':
71
+ result = this.handleSessionSetMode(params ?? {});
72
+ break;
73
+ case 'session/set_config_option':
74
+ result = {};
75
+ break;
76
+ case 'session/close':
77
+ result = this.handleSessionClose(params ?? {});
78
+ break;
79
+ case 'logout':
80
+ this.authenticated = false;
81
+ result = {};
82
+ break;
83
+ // Document sync notifications — acknowledge silently
84
+ case 'document/didOpen':
85
+ case 'document/didChange':
86
+ case 'document/didClose':
87
+ case 'document/didSave':
88
+ case 'document/didFocus':
89
+ return; // Notifications, no response
90
+ default:
91
+ if (isNotification)
92
+ return;
93
+ this.sendError(id, METHOD_NOT_FOUND, `Unknown method: ${method}`);
94
+ return;
95
+ }
96
+ if (!isNotification) {
97
+ this.sendResult(id, result);
98
+ }
99
+ }
100
+ catch (err) {
101
+ if (!isNotification) {
102
+ if (err instanceof JsonRpcError) {
103
+ this.sendError(id, err.code, err.message, err.data);
104
+ }
105
+ else {
106
+ const message = err instanceof Error ? err.message : String(err);
107
+ this.sendError(id, INTERNAL_ERROR, message);
108
+ }
109
+ }
110
+ else {
111
+ process.stderr.write(`[acp] Error handling notification ${method}: ${err}\n`);
112
+ }
113
+ }
114
+ }
115
+ // ── Method handlers ────────────────────────────────────────────
116
+ async handleInitialize(_params) {
117
+ // Pre-fetch agent info for capabilities
118
+ try {
119
+ this.agentInfo = await this.proxy.getAgent(this.agentId);
120
+ }
121
+ catch (err) {
122
+ process.stderr.write(`[acp] Warning: could not fetch agent info: ${err}\n`);
123
+ }
124
+ const hasPipeline = this.agentInfo?.mode === 'workflow' && this.agentInfo.pipeline?.nodes?.length;
125
+ return {
126
+ protocolVersion: 1,
127
+ agentCapabilities: {
128
+ tools: true,
129
+ streaming: true,
130
+ sessions: true,
131
+ sessionList: true,
132
+ plan: !!hasPipeline,
133
+ modes: false,
134
+ },
135
+ authMethods: [
136
+ {
137
+ id: 'almyty_token',
138
+ type: 'env_var',
139
+ label: 'almyty API token',
140
+ envVars: ['ALMYTY_TOKEN'],
141
+ },
142
+ ],
143
+ agentInfo: this.agentInfo
144
+ ? { name: this.agentInfo.name, description: this.agentInfo.description }
145
+ : { name: 'almyty agent' },
146
+ };
147
+ }
148
+ async handleAuthenticate(params) {
149
+ const methodId = params.methodId;
150
+ if (methodId === 'almyty_token') {
151
+ // The token is already configured via env var or credentials file.
152
+ // Validate by making a lightweight API call.
153
+ try {
154
+ await this.proxy.getAgent(this.agentId);
155
+ this.authenticated = true;
156
+ return { authenticated: true };
157
+ }
158
+ catch {
159
+ return { authenticated: false };
160
+ }
161
+ }
162
+ // Unknown auth method — the token was pre-configured at startup,
163
+ // so we consider it authenticated if we can reach the backend.
164
+ try {
165
+ await this.proxy.getAgent(this.agentId);
166
+ this.authenticated = true;
167
+ return { authenticated: true };
168
+ }
169
+ catch {
170
+ return { authenticated: false };
171
+ }
172
+ }
173
+ async handleSessionNew(params) {
174
+ const mode = this.agentInfo?.mode ?? 'workflow';
175
+ const session = this.sessions.create(this.agentId, mode);
176
+ process.stderr.write(`[acp] Session created: ${session.id} (agent: ${this.agentId}, mode: ${mode})\n`);
177
+ // For workflow agents with a pipeline, send the plan
178
+ if (mode === 'workflow' && this.agentInfo?.pipeline?.nodes?.length) {
179
+ const plan = buildPlanFromPipeline(this.agentInfo.pipeline.nodes);
180
+ this.sendNotification('session/update', {
181
+ sessionId: session.id,
182
+ update: plan,
183
+ });
184
+ }
185
+ return { sessionId: session.id };
186
+ }
187
+ async handleSessionList() {
188
+ const sessions = this.sessions.list().map((s) => ({
189
+ id: s.id,
190
+ agentId: s.agentId,
191
+ mode: s.mode,
192
+ }));
193
+ return { sessions };
194
+ }
195
+ async handleSessionLoad(params) {
196
+ const sessionId = params.sessionId;
197
+ if (!sessionId) {
198
+ throw new JsonRpcError(INVALID_PARAMS, 'sessionId is required');
199
+ }
200
+ const session = this.sessions.get(sessionId);
201
+ if (!session) {
202
+ throw new JsonRpcError(INVALID_PARAMS, `Session not found: ${sessionId}`);
203
+ }
204
+ return { sessionId: session.id };
205
+ }
206
+ async handleSessionPrompt(params) {
207
+ const sessionId = params.sessionId;
208
+ if (!sessionId) {
209
+ throw new JsonRpcError(INVALID_PARAMS, 'sessionId is required');
210
+ }
211
+ const session = this.sessions.get(sessionId);
212
+ if (!session) {
213
+ throw new JsonRpcError(INVALID_PARAMS, `Session not found: ${sessionId}`);
214
+ }
215
+ // Extract text from ContentBlock array
216
+ const content = params.content;
217
+ const inputText = extractTextFromContent(content);
218
+ if (!inputText) {
219
+ throw new JsonRpcError(INVALID_PARAMS, 'No text content in prompt');
220
+ }
221
+ // Set up abort controller for this prompt turn
222
+ const abortController = new AbortController();
223
+ this.sessions.update(sessionId, { abortController });
224
+ try {
225
+ if (session.mode === 'autonomous') {
226
+ return await this.handleAutonomousPrompt(session, inputText, abortController.signal);
227
+ }
228
+ else {
229
+ return await this.handleWorkflowPrompt(session, inputText, abortController.signal);
230
+ }
231
+ }
232
+ finally {
233
+ this.sessions.update(sessionId, { abortController: undefined, activeRunId: undefined });
234
+ }
235
+ }
236
+ /**
237
+ * Handle a prompt for a workflow agent: POST /agents/:id/stream.
238
+ * Streams events via SSE and maps them to ACP session updates.
239
+ */
240
+ async handleWorkflowPrompt(session, input, signal) {
241
+ const collectedText = [];
242
+ try {
243
+ for await (const event of this.proxy.streamAgent(session.agentId, { input }, signal)) {
244
+ if (signal.aborted) {
245
+ return { stopReason: 'cancelled' };
246
+ }
247
+ const updates = mapStreamEvent(event);
248
+ for (const update of updates) {
249
+ this.sendSessionUpdate(session.id, update);
250
+ if (update.type === 'agent_message_chunk') {
251
+ collectedText.push(update.text);
252
+ }
253
+ }
254
+ if (isTerminalEvent(event)) {
255
+ const finalOutput = extractFinalOutput(event);
256
+ if (finalOutput && !collectedText.includes(finalOutput)) {
257
+ collectedText.push(finalOutput);
258
+ }
259
+ if (event.event === 'execution.error' || event.event === 'run.error') {
260
+ return {
261
+ stopReason: 'end_turn',
262
+ content: [{ type: 'text', text: collectedText.join('') || 'Execution failed.' }],
263
+ };
264
+ }
265
+ }
266
+ }
267
+ }
268
+ catch (err) {
269
+ if (signal.aborted) {
270
+ return { stopReason: 'cancelled' };
271
+ }
272
+ // If streaming fails, fall back to synchronous invoke
273
+ process.stderr.write(`[acp] Streaming failed, falling back to invoke: ${err}\n`);
274
+ try {
275
+ const result = await this.proxy.invokeAgent(session.agentId, { input });
276
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
277
+ this.sendSessionUpdate(session.id, {
278
+ type: 'agent_message_chunk',
279
+ text,
280
+ });
281
+ return {
282
+ stopReason: 'end_turn',
283
+ content: [{ type: 'text', text }],
284
+ };
285
+ }
286
+ catch (invokeErr) {
287
+ const errMsg = invokeErr instanceof Error ? invokeErr.message : String(invokeErr);
288
+ return {
289
+ stopReason: 'end_turn',
290
+ content: [{ type: 'text', text: `Error: ${errMsg}` }],
291
+ };
292
+ }
293
+ }
294
+ return {
295
+ stopReason: 'end_turn',
296
+ content: collectedText.length > 0
297
+ ? [{ type: 'text', text: collectedText.join('') }]
298
+ : undefined,
299
+ };
300
+ }
301
+ /**
302
+ * Handle a prompt for an autonomous agent: start or continue a run.
303
+ * If the session has no active conversation, starts a new run.
304
+ * If the agent is waiting for input, sends input to the existing run.
305
+ */
306
+ async handleAutonomousPrompt(session, input, signal) {
307
+ const collectedText = [];
308
+ try {
309
+ let run;
310
+ if (session.activeRunId) {
311
+ // Continue an existing run that is waiting for input
312
+ run = await this.proxy.sendRunInput(session.agentId, session.activeRunId, { message: input });
313
+ }
314
+ else {
315
+ // Start a new run
316
+ run = await this.proxy.startRun(session.agentId, { message: input });
317
+ this.sessions.update(session.id, {
318
+ activeRunId: run.id,
319
+ conversationId: run.conversationId,
320
+ });
321
+ }
322
+ process.stderr.write(`[acp] Run started: ${run.id} (status: ${run.status})\n`);
323
+ // Stream events from the run
324
+ try {
325
+ for await (const event of this.proxy.streamRun(session.agentId, run.id, signal)) {
326
+ if (signal.aborted) {
327
+ // Try to cancel the run on the backend
328
+ try {
329
+ await this.proxy.cancelRun(session.agentId, run.id);
330
+ }
331
+ catch { /* best effort */ }
332
+ return { stopReason: 'cancelled' };
333
+ }
334
+ const updates = mapStreamEvent(event);
335
+ for (const update of updates) {
336
+ this.sendSessionUpdate(session.id, update);
337
+ if (update.type === 'agent_message_chunk') {
338
+ collectedText.push(update.text);
339
+ }
340
+ }
341
+ if (isTerminalEvent(event)) {
342
+ const finalOutput = extractFinalOutput(event);
343
+ if (finalOutput && !collectedText.includes(finalOutput)) {
344
+ collectedText.push(finalOutput);
345
+ }
346
+ break;
347
+ }
348
+ }
349
+ }
350
+ catch (streamErr) {
351
+ // If streaming fails, fall back to polling the run status
352
+ process.stderr.write(`[acp] Run streaming failed, polling: ${streamErr}\n`);
353
+ const finalRun = await this.pollRunToCompletion(session.agentId, run.id, signal);
354
+ if (finalRun.output) {
355
+ const text = typeof finalRun.output === 'string'
356
+ ? finalRun.output
357
+ : JSON.stringify(finalRun.output, null, 2);
358
+ collectedText.push(text);
359
+ this.sendSessionUpdate(session.id, {
360
+ type: 'agent_message_chunk',
361
+ text,
362
+ });
363
+ }
364
+ if (finalRun.error) {
365
+ collectedText.push(`Error: ${finalRun.error}`);
366
+ }
367
+ if (finalRun.status === 'waiting_input') {
368
+ // Agent needs more input — keep the run active
369
+ this.sessions.update(session.id, { activeRunId: finalRun.id });
370
+ return {
371
+ stopReason: 'end_turn',
372
+ content: collectedText.length > 0
373
+ ? [{ type: 'text', text: collectedText.join('') }]
374
+ : [{ type: 'text', text: 'The agent is waiting for your input.' }],
375
+ };
376
+ }
377
+ }
378
+ // Check final run status — if still running, poll until complete
379
+ let finalRun = await this.proxy.getRun(session.agentId, run.id).catch(() => run);
380
+ if (finalRun.status === 'running') {
381
+ process.stderr.write(`[acp] Run still running after stream ended, polling...\n`);
382
+ finalRun = await this.pollRunToCompletion(session.agentId, run.id, signal);
383
+ if (finalRun.output) {
384
+ const text = typeof finalRun.output === 'string'
385
+ ? finalRun.output
386
+ : JSON.stringify(finalRun.output, null, 2);
387
+ if (!collectedText.includes(text)) {
388
+ collectedText.push(text);
389
+ this.sendSessionUpdate(session.id, { type: 'agent_message_chunk', text });
390
+ }
391
+ }
392
+ if (finalRun.error) {
393
+ const errText = typeof finalRun.error === 'string'
394
+ ? finalRun.error
395
+ : JSON.stringify(finalRun.error);
396
+ collectedText.push(`Error: ${errText}`);
397
+ }
398
+ }
399
+ if (finalRun.status === 'waiting_input') {
400
+ this.sessions.update(session.id, { activeRunId: finalRun.id });
401
+ }
402
+ else {
403
+ this.sessions.update(session.id, { activeRunId: undefined });
404
+ }
405
+ if (finalRun.status === 'cancelled') {
406
+ return { stopReason: 'cancelled' };
407
+ }
408
+ return {
409
+ stopReason: finalRun.status === 'failed' ? 'refusal' : 'end_turn',
410
+ content: collectedText.length > 0
411
+ ? [{ type: 'text', text: collectedText.join('') }]
412
+ : undefined,
413
+ };
414
+ }
415
+ catch (err) {
416
+ if (signal.aborted) {
417
+ return { stopReason: 'cancelled' };
418
+ }
419
+ const errMsg = err instanceof Error ? err.message : String(err);
420
+ return {
421
+ stopReason: 'end_turn',
422
+ content: [{ type: 'text', text: `Error: ${errMsg}` }],
423
+ };
424
+ }
425
+ }
426
+ /**
427
+ * Poll a run until it reaches a terminal state or the signal is aborted.
428
+ */
429
+ async pollRunToCompletion(agentId, runId, signal) {
430
+ const POLL_INTERVAL_MS = 1000;
431
+ const MAX_POLLS = 300; // 5 minutes max
432
+ for (let i = 0; i < MAX_POLLS; i++) {
433
+ if (signal.aborted) {
434
+ throw new Error('Cancelled');
435
+ }
436
+ const run = await this.proxy.getRun(agentId, runId);
437
+ if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled' || run.status === 'waiting_input') {
438
+ return run;
439
+ }
440
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
441
+ }
442
+ throw new Error('Run polling timed out');
443
+ }
444
+ handleSessionCancel(params) {
445
+ const sessionId = params.sessionId;
446
+ if (sessionId) {
447
+ process.stderr.write(`[acp] Cancelling session: ${sessionId}\n`);
448
+ this.sessions.cancel(sessionId);
449
+ }
450
+ }
451
+ handleSessionSetMode(params) {
452
+ const sessionId = params.sessionId;
453
+ const mode = params.mode;
454
+ if (sessionId && mode) {
455
+ this.sessions.update(sessionId, { userMode: mode });
456
+ }
457
+ // Acknowledge but no-op — almyty agents don't have client-side modes
458
+ return {};
459
+ }
460
+ handleSessionClose(params) {
461
+ const sessionId = params.sessionId;
462
+ if (!sessionId) {
463
+ throw new JsonRpcError(INVALID_PARAMS, 'sessionId is required');
464
+ }
465
+ const closed = this.sessions.close(sessionId);
466
+ process.stderr.write(`[acp] Session closed: ${sessionId} (found: ${closed})\n`);
467
+ return { closed };
468
+ }
469
+ // ── Message sending helpers ────────────────────────────────────
470
+ sendResult(id, result) {
471
+ this.send({
472
+ jsonrpc: '2.0',
473
+ id,
474
+ result,
475
+ });
476
+ }
477
+ sendError(id, code, message, data) {
478
+ this.send({
479
+ jsonrpc: '2.0',
480
+ id,
481
+ error: { code, message, data },
482
+ });
483
+ }
484
+ sendNotification(method, params) {
485
+ this.send({
486
+ jsonrpc: '2.0',
487
+ method,
488
+ params,
489
+ });
490
+ }
491
+ sendSessionUpdate(sessionId, update) {
492
+ this.sendNotification('session/update', {
493
+ sessionId,
494
+ update,
495
+ });
496
+ }
497
+ /**
498
+ * Clean up all sessions. Called during shutdown.
499
+ */
500
+ shutdown() {
501
+ this.sessions.closeAll();
502
+ }
503
+ }
504
+ // ── Helpers ──────────────────────────────────────────────────────
505
+ /**
506
+ * Extract plain text from an array of ACP ContentBlocks.
507
+ */
508
+ function extractTextFromContent(content) {
509
+ if (!content || !Array.isArray(content))
510
+ return '';
511
+ return content
512
+ .filter((block) => block.type === 'text' && block.text)
513
+ .map((block) => block.text)
514
+ .join('\n');
515
+ }
516
+ /**
517
+ * Typed JSON-RPC error for structured error responses.
518
+ */
519
+ class JsonRpcError extends Error {
520
+ code;
521
+ data;
522
+ constructor(code, message, data) {
523
+ super(message);
524
+ this.code = code;
525
+ this.data = data;
526
+ }
527
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Credential resolver for @almyty/acp-server.
3
+ *
4
+ * Reads the shared credential store written by `npx @almyty/auth login`.
5
+ * Lookup order: ALMYTY_TOKEN env var, then ~/.almyty/credentials.json.
6
+ */
7
+ interface StoredCredentials {
8
+ url: string;
9
+ token: string;
10
+ email?: string;
11
+ frontendUrl?: string;
12
+ }
13
+ export declare function loadCredentials(): StoredCredentials | null;
14
+ /**
15
+ * Resolve token: env var > stored credentials. Returns null when no
16
+ * credentials are available.
17
+ */
18
+ export declare function resolveCredentials(): {
19
+ url: string;
20
+ token: string;
21
+ } | null;
22
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Credential resolver for @almyty/acp-server.
3
+ *
4
+ * Reads the shared credential store written by `npx @almyty/auth login`.
5
+ * Lookup order: ALMYTY_TOKEN env var, then ~/.almyty/credentials.json.
6
+ */
7
+ import { readFileSync, existsSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ const CREDENTIALS_FILE = join(homedir(), '.almyty', 'credentials.json');
11
+ export function loadCredentials() {
12
+ try {
13
+ if (!existsSync(CREDENTIALS_FILE))
14
+ return null;
15
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * Resolve token: env var > stored credentials. Returns null when no
23
+ * credentials are available.
24
+ */
25
+ export function resolveCredentials() {
26
+ const envToken = process.env.ALMYTY_TOKEN;
27
+ const envUrl = process.env.ALMYTY_URL || 'https://api.almyty.com';
28
+ if (envToken) {
29
+ return { url: envUrl, token: envToken };
30
+ }
31
+ const creds = loadCredentials();
32
+ if (creds?.token) {
33
+ return { url: creds.url, token: creds.token };
34
+ }
35
+ return null;
36
+ }
@@ -0,0 +1,76 @@
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
+ import type { StreamEvent, PipelineNode } from './proxy.js';
9
+ export interface SessionUpdate {
10
+ sessionId: string;
11
+ update: SessionUpdatePayload;
12
+ }
13
+ export type SessionUpdatePayload = AgentMessageChunk | AgentThoughtChunk | ToolCall | ToolCallUpdate | Plan;
14
+ export interface AgentMessageChunk {
15
+ type: 'agent_message_chunk';
16
+ /** Incremental text content. */
17
+ text: string;
18
+ /** MIME type (default text/plain). */
19
+ mimeType?: string;
20
+ }
21
+ export interface AgentThoughtChunk {
22
+ type: 'agent_thought_chunk';
23
+ text: string;
24
+ }
25
+ export interface ToolCall {
26
+ type: 'tool_call';
27
+ /** Unique ID for this tool invocation (for pairing with updates). */
28
+ toolCallId: string;
29
+ /** Human-readable title (tool name or node label). */
30
+ title: string;
31
+ /** Tool call kind: read, edit, execute, fetch, think, other. */
32
+ kind: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
33
+ /** Current status. */
34
+ status: 'running' | 'completed' | 'error' | 'cancelled';
35
+ /** Raw input parameters (stringified JSON). */
36
+ rawInput?: string;
37
+ }
38
+ export interface ToolCallUpdate {
39
+ type: 'tool_call_update';
40
+ /** Matches the toolCallId from the original ToolCall. */
41
+ toolCallId: string;
42
+ /** Updated status. */
43
+ status: 'running' | 'completed' | 'error' | 'cancelled';
44
+ /** Output content (on completion). */
45
+ content?: string;
46
+ /** Error message (on failure). */
47
+ error?: string;
48
+ }
49
+ export interface PlanEntry {
50
+ title: string;
51
+ priority: 'high' | 'medium' | 'low';
52
+ status: 'todo' | 'in_progress' | 'done' | 'error';
53
+ }
54
+ export interface Plan {
55
+ type: 'plan';
56
+ entries: PlanEntry[];
57
+ }
58
+ /**
59
+ * Convert a single almyty StreamEvent into zero or more ACP SessionUpdate
60
+ * payloads. Most events map 1:1, but some (like node.completed with output)
61
+ * may produce both a tool_call_update and an agent_message_chunk.
62
+ */
63
+ export declare function mapStreamEvent(event: StreamEvent): SessionUpdatePayload[];
64
+ /**
65
+ * Build an ACP Plan from almyty pipeline nodes. Maps node types to
66
+ * priorities and initializes all entries as "todo".
67
+ */
68
+ export declare function buildPlanFromPipeline(nodes: PipelineNode[]): Plan;
69
+ /**
70
+ * Check if a StreamEvent indicates the execution has finished.
71
+ */
72
+ export declare function isTerminalEvent(event: StreamEvent): boolean;
73
+ /**
74
+ * Extract final output text from a terminal StreamEvent.
75
+ */
76
+ export declare function extractFinalOutput(event: StreamEvent): string | undefined;