nexus-agentd 0.2.2 → 0.2.3
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/acp/runtime.d.ts +3 -0
- package/dist/acp/runtime.js +119 -2
- package/dist/completion-contract.js +1 -1
- package/dist/session-contract.d.ts +1 -0
- package/dist/session.d.ts +1 -0
- package/dist/session.js +5 -0
- package/package.json +1 -1
package/dist/acp/runtime.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class AcpProcessRuntime {
|
|
|
14
14
|
private readonly promptTimeoutMs;
|
|
15
15
|
private promptCapabilities;
|
|
16
16
|
private inputDirectory?;
|
|
17
|
+
private workspace?;
|
|
18
|
+
private promptOutput;
|
|
17
19
|
constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number);
|
|
18
20
|
start(workspace: string): Promise<void>;
|
|
19
21
|
prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
@@ -27,6 +29,7 @@ export declare class AcpProcessRuntime {
|
|
|
27
29
|
private sessionUpdate;
|
|
28
30
|
private captureToolArtifacts;
|
|
29
31
|
private captureArtifact;
|
|
32
|
+
private captureMediaMarkers;
|
|
30
33
|
private captureStderr;
|
|
31
34
|
private onProcessFailure;
|
|
32
35
|
private requireSessionId;
|
package/dist/acp/runtime.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Readable, Writable } from 'node:stream';
|
|
5
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import * as acp from '@agentclientprotocol/sdk';
|
|
7
7
|
import { terminateProcessTree } from '../process-tree.js';
|
|
8
8
|
import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
|
|
9
|
+
const MAX_MEDIA_MARKER_FILE_BYTES = 32 * 1024 * 1024;
|
|
9
10
|
export class AcpProcessRuntime {
|
|
10
11
|
driver;
|
|
11
12
|
sink;
|
|
@@ -19,6 +20,8 @@ export class AcpProcessRuntime {
|
|
|
19
20
|
promptTimeoutMs;
|
|
20
21
|
promptCapabilities = {};
|
|
21
22
|
inputDirectory;
|
|
23
|
+
workspace;
|
|
24
|
+
promptOutput = '';
|
|
22
25
|
constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
|
|
23
26
|
this.driver = driver;
|
|
24
27
|
this.sink = sink;
|
|
@@ -31,6 +34,7 @@ export class AcpProcessRuntime {
|
|
|
31
34
|
async start(workspace) {
|
|
32
35
|
if (this.process)
|
|
33
36
|
throw new Error('ACP runtime is already started');
|
|
37
|
+
this.workspace = workspace;
|
|
34
38
|
this.inputDirectory = path.join(workspace, '.nexus-inputs', this.sink.id);
|
|
35
39
|
const child = this.driver.spawn(workspace);
|
|
36
40
|
this.process = child;
|
|
@@ -86,6 +90,7 @@ export class AcpProcessRuntime {
|
|
|
86
90
|
throw new Error('ACP session is already processing a prompt');
|
|
87
91
|
const sessionId = this.requireSessionId();
|
|
88
92
|
this.prompting = true;
|
|
93
|
+
this.promptOutput = '';
|
|
89
94
|
this.sink.clearPending();
|
|
90
95
|
this.sink.setState('running');
|
|
91
96
|
try {
|
|
@@ -98,6 +103,7 @@ export class AcpProcessRuntime {
|
|
|
98
103
|
}
|
|
99
104
|
switch (response.stopReason) {
|
|
100
105
|
case 'end_turn':
|
|
106
|
+
await this.captureMediaMarkers();
|
|
101
107
|
this.sink.completeTurn({
|
|
102
108
|
source: 'acp_prompt_response',
|
|
103
109
|
stopReason: response.stopReason
|
|
@@ -356,6 +362,7 @@ export class AcpProcessRuntime {
|
|
|
356
362
|
switch (update.sessionUpdate) {
|
|
357
363
|
case 'agent_message_chunk':
|
|
358
364
|
if (update.content.type === 'text') {
|
|
365
|
+
this.promptOutput += update.content.text;
|
|
359
366
|
this.sink.appendOutput(update.content.text);
|
|
360
367
|
this.sink.emit('assistant_chunk', {
|
|
361
368
|
messageId: update.messageId,
|
|
@@ -410,6 +417,29 @@ export class AcpProcessRuntime {
|
|
|
410
417
|
if (artifact)
|
|
411
418
|
this.sink.addArtifact(artifact);
|
|
412
419
|
}
|
|
420
|
+
async captureMediaMarkers() {
|
|
421
|
+
const markers = mediaMarkers(this.promptOutput);
|
|
422
|
+
if (!markers.length)
|
|
423
|
+
return;
|
|
424
|
+
this.sink.replaceOutput(withoutMediaMarkers(this.promptOutput));
|
|
425
|
+
const seen = new Set();
|
|
426
|
+
for (const marker of markers) {
|
|
427
|
+
if (seen.has(marker))
|
|
428
|
+
continue;
|
|
429
|
+
seen.add(marker);
|
|
430
|
+
try {
|
|
431
|
+
const artifact = await artifactFromMediaMarker(marker, `media:${randomUUID()}`, this.workspace);
|
|
432
|
+
this.sink.addArtifact(artifact);
|
|
433
|
+
this.sink.emit('artifact', artifact);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
this.sink.emit('terminal_output', {
|
|
437
|
+
stream: 'system',
|
|
438
|
+
text: `Could not attach MEDIA artifact ${safeMarkerName(marker)}: ${error instanceof Error ? error.message : String(error)}`
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
413
443
|
captureStderr(child) {
|
|
414
444
|
let buffer = '';
|
|
415
445
|
let flushed = false;
|
|
@@ -619,6 +649,93 @@ function filenameFromValue(value) {
|
|
|
619
649
|
function usableUrl(value) {
|
|
620
650
|
return value && /^(?:https?|file):/i.test(value) ? value : undefined;
|
|
621
651
|
}
|
|
652
|
+
function mediaMarkers(output) {
|
|
653
|
+
return output
|
|
654
|
+
.split(/\r?\n/)
|
|
655
|
+
.map((line) => line.match(/^\s*MEDIA:\s*(.+?)\s*$/i)?.[1]?.trim())
|
|
656
|
+
.filter((value) => Boolean(value));
|
|
657
|
+
}
|
|
658
|
+
function withoutMediaMarkers(output) {
|
|
659
|
+
return output
|
|
660
|
+
.split(/\r?\n/)
|
|
661
|
+
.filter((line) => !/^\s*MEDIA:\s*(.+?)\s*$/i.test(line))
|
|
662
|
+
.join('\n')
|
|
663
|
+
.trim();
|
|
664
|
+
}
|
|
665
|
+
async function artifactFromMediaMarker(marker, id, workspace) {
|
|
666
|
+
if (/^https?:\/\//i.test(marker)) {
|
|
667
|
+
const filename = artifactFilename(marker, undefined, 'resource', undefined);
|
|
668
|
+
return {
|
|
669
|
+
id,
|
|
670
|
+
name: filename,
|
|
671
|
+
filename,
|
|
672
|
+
url: marker,
|
|
673
|
+
metadata: { source: 'acp_media_marker' }
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const localPath = marker.startsWith('file:')
|
|
677
|
+
? fileURLToPath(marker)
|
|
678
|
+
: marker;
|
|
679
|
+
const resolved = await realpath(path.isAbsolute(localPath)
|
|
680
|
+
? localPath
|
|
681
|
+
: path.resolve(workspace || process.cwd(), localPath));
|
|
682
|
+
const details = await stat(resolved);
|
|
683
|
+
if (!details.isFile()) {
|
|
684
|
+
throw new Error('declared path is not a regular file');
|
|
685
|
+
}
|
|
686
|
+
if (details.size > MAX_MEDIA_MARKER_FILE_BYTES) {
|
|
687
|
+
throw new Error(`declared file exceeds the ${MAX_MEDIA_MARKER_FILE_BYTES} byte limit`);
|
|
688
|
+
}
|
|
689
|
+
const filename = path.basename(resolved);
|
|
690
|
+
return {
|
|
691
|
+
id,
|
|
692
|
+
name: filename,
|
|
693
|
+
filename,
|
|
694
|
+
mediaType: mediaTypeForPath(filename),
|
|
695
|
+
bytesBase64: (await readFile(resolved)).toString('base64'),
|
|
696
|
+
metadata: {
|
|
697
|
+
source: 'acp_media_marker',
|
|
698
|
+
size: details.size
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function safeMarkerName(marker) {
|
|
703
|
+
try {
|
|
704
|
+
return path.basename(marker.startsWith('file:') ? fileURLToPath(marker) : marker);
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return 'declared artifact';
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function mediaTypeForPath(value) {
|
|
711
|
+
switch (path.extname(value).toLowerCase()) {
|
|
712
|
+
case '.pptx':
|
|
713
|
+
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
714
|
+
case '.docx':
|
|
715
|
+
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
716
|
+
case '.xlsx':
|
|
717
|
+
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
718
|
+
case '.pdf':
|
|
719
|
+
return 'application/pdf';
|
|
720
|
+
case '.png':
|
|
721
|
+
return 'image/png';
|
|
722
|
+
case '.jpg':
|
|
723
|
+
case '.jpeg':
|
|
724
|
+
return 'image/jpeg';
|
|
725
|
+
case '.gif':
|
|
726
|
+
return 'image/gif';
|
|
727
|
+
case '.webp':
|
|
728
|
+
return 'image/webp';
|
|
729
|
+
case '.mp3':
|
|
730
|
+
return 'audio/mpeg';
|
|
731
|
+
case '.wav':
|
|
732
|
+
return 'audio/wav';
|
|
733
|
+
case '.mp4':
|
|
734
|
+
return 'video/mp4';
|
|
735
|
+
default:
|
|
736
|
+
return 'application/octet-stream';
|
|
737
|
+
}
|
|
738
|
+
}
|
|
622
739
|
function artifactIndex(id) {
|
|
623
740
|
return id.split(':').pop() || 'output';
|
|
624
741
|
}
|
|
@@ -4,6 +4,6 @@ export const TURN_COMPLETION_CONTRACT = [
|
|
|
4
4
|
'If you need user input or permission, use the protocol input/permission request and wait for the response.',
|
|
5
5
|
'Before ending the turn, verify the requested work and provide a non-empty final response.',
|
|
6
6
|
'The final response must summarize the outcome, verification performed, and workspace-relative paths for generated files.',
|
|
7
|
-
'Attach generated resources or artifacts through the protocol when supported.',
|
|
7
|
+
'Attach generated resources or artifacts through the protocol when supported. If the runtime only reports a local deliverable path, put each requested file on its own final line as MEDIA:<path>; Agent Nexus will snapshot that explicitly declared file as an artifact before the turn completes.',
|
|
8
8
|
'</agent_nexus_completion_contract>'
|
|
9
9
|
].join('\n');
|
|
@@ -7,6 +7,7 @@ export interface AgentSessionSink {
|
|
|
7
7
|
setState(state: AgentdSessionState, error?: string): void;
|
|
8
8
|
completeTurn(proof: AgentdTurnCompletionProof): boolean;
|
|
9
9
|
appendOutput(text: string): void;
|
|
10
|
+
replaceOutput(text: string): void;
|
|
10
11
|
addArtifact(artifact: AgentdArtifact): void;
|
|
11
12
|
setPending(request: AgentdPendingRequest): void;
|
|
12
13
|
clearPending(): void;
|
package/dist/session.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare class ManagedSession implements AgentSessionSink, AcpSessionSink
|
|
|
34
34
|
setState(state: AgentdSessionState, error?: string): void;
|
|
35
35
|
completeTurn(proof: AgentdTurnCompletionProof): boolean;
|
|
36
36
|
appendOutput(text: string): void;
|
|
37
|
+
replaceOutput(text: string): void;
|
|
37
38
|
addArtifact(artifact: AgentdArtifact): void;
|
|
38
39
|
publishFile(filePath: string): Promise<AgentdArtifact>;
|
|
39
40
|
setPending(request: AgentdPendingRequest): void;
|
package/dist/session.js
CHANGED
|
@@ -130,6 +130,11 @@ export class ManagedSession {
|
|
|
130
130
|
this.updatedAt = Date.now();
|
|
131
131
|
this.syncRun({ output: this.output });
|
|
132
132
|
}
|
|
133
|
+
replaceOutput(text) {
|
|
134
|
+
this.output = String(text || '').slice(0, this.maxOutputChars);
|
|
135
|
+
this.updatedAt = Date.now();
|
|
136
|
+
this.syncRun({ output: this.output || undefined });
|
|
137
|
+
}
|
|
133
138
|
addArtifact(artifact) {
|
|
134
139
|
const next = structuredClone(artifact);
|
|
135
140
|
const existingIndex = next.id
|