nexus-agentd 0.1.8 → 0.2.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/LICENSE +22 -22
- package/README.md +224 -446
- package/dist/a2a/runtime.d.ts +5 -4
- package/dist/a2a/runtime.js +83 -13
- package/dist/acp/runtime.d.ts +3 -8
- package/dist/acp/runtime.js +63 -79
- package/dist/completion-contract.d.ts +1 -0
- package/dist/completion-contract.js +9 -0
- package/dist/config.js +1 -15
- package/dist/control-plane.d.ts +0 -1
- package/dist/control-plane.js +1 -19
- package/dist/run-store.d.ts +1 -1
- package/dist/server.js +63 -248
- package/dist/session-contract.d.ts +3 -2
- package/dist/session.d.ts +11 -2
- package/dist/session.js +202 -17
- package/dist/types.d.ts +25 -45
- package/dist/webui/app.js +912 -1074
- package/dist/webui/icons.d.ts +0 -1
- package/dist/webui/icons.js +0 -1
- package/dist/webui/index.js +10 -10
- package/dist/webui/markup.js +65 -66
- package/dist/webui/styles.js +256 -268
- package/nexus-agentd.example.json +40 -47
- package/package.json +52 -52
- package/dist/agent-instructions.d.ts +0 -11
- package/dist/agent-instructions.js +0 -44
- package/dist/artifact-store.d.ts +0 -41
- package/dist/artifact-store.js +0 -340
- package/dist/workspace-files.d.ts +0 -48
- package/dist/workspace-files.js +0 -269
package/dist/a2a/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentSessionRuntime, AgentSessionSink } from '../session-contract.js';
|
|
2
|
-
import type { AgentdA2AConfig, AgentdAgentView, AgentdInputAttachment } from '../types.js';
|
|
2
|
+
import type { AgentdA2AConfig, AgentdAgentView, AgentdInputAttachment, AgentdPendingResponse } from '../types.js';
|
|
3
3
|
export declare function probeA2AAgent(id: string, config: AgentdA2AConfig): Promise<AgentdAgentView>;
|
|
4
4
|
export declare class A2AClientRuntime implements AgentSessionRuntime {
|
|
5
5
|
private readonly config;
|
|
@@ -10,16 +10,17 @@ export declare class A2AClientRuntime implements AgentSessionRuntime {
|
|
|
10
10
|
private taskId;
|
|
11
11
|
private contextId;
|
|
12
12
|
private prompting;
|
|
13
|
-
private firstPrompt;
|
|
14
|
-
private readonly instructions;
|
|
15
13
|
private disposed;
|
|
16
14
|
private activeController?;
|
|
17
15
|
private readonly artifactCache;
|
|
18
16
|
private readonly emittedMessages;
|
|
17
|
+
private sawTaskStatus;
|
|
18
|
+
private sawAgentMessage;
|
|
19
|
+
private completionProof?;
|
|
19
20
|
constructor(config: AgentdA2AConfig, sink: AgentSessionSink, promptTimeoutMs: number);
|
|
20
21
|
start(): Promise<void>;
|
|
21
22
|
prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
22
|
-
respondPending(
|
|
23
|
+
respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
23
24
|
cancel(): Promise<void>;
|
|
24
25
|
dispose(): Promise<void>;
|
|
25
26
|
private messageRequest;
|
package/dist/a2a/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { Role, TaskState } from '@a2a-js/sdk';
|
|
3
3
|
import { ClientFactory, DefaultAgentCardResolver, JsonRpcTransportFactory, RestTransportFactory } from '@a2a-js/sdk/client';
|
|
4
|
-
import {
|
|
4
|
+
import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
|
|
5
5
|
export async function probeA2AAgent(id, config) {
|
|
6
6
|
const startedAt = Date.now();
|
|
7
7
|
if (config.enabled === false)
|
|
@@ -44,17 +44,17 @@ export class A2AClientRuntime {
|
|
|
44
44
|
taskId = '';
|
|
45
45
|
contextId = '';
|
|
46
46
|
prompting = false;
|
|
47
|
-
firstPrompt = true;
|
|
48
|
-
instructions;
|
|
49
47
|
disposed = false;
|
|
50
48
|
activeController;
|
|
51
49
|
artifactCache = new Map();
|
|
52
50
|
emittedMessages = new Set();
|
|
51
|
+
sawTaskStatus = false;
|
|
52
|
+
sawAgentMessage = false;
|
|
53
|
+
completionProof;
|
|
53
54
|
constructor(config, sink, promptTimeoutMs) {
|
|
54
55
|
this.config = config;
|
|
55
56
|
this.sink = sink;
|
|
56
57
|
this.promptTimeoutMs = promptTimeoutMs;
|
|
57
|
-
this.instructions = buildAgentInstructions(config.instructions);
|
|
58
58
|
}
|
|
59
59
|
async start() {
|
|
60
60
|
const resolver = createResolver(this.config);
|
|
@@ -80,25 +80,38 @@ export class A2AClientRuntime {
|
|
|
80
80
|
if (this.disposed)
|
|
81
81
|
throw new Error('A2A runtime is disposed');
|
|
82
82
|
this.prompting = true;
|
|
83
|
+
this.sawTaskStatus = false;
|
|
84
|
+
this.sawAgentMessage = false;
|
|
85
|
+
this.completionProof = undefined;
|
|
83
86
|
this.sink.clearPending();
|
|
84
87
|
this.sink.setState('running');
|
|
85
|
-
const promptMessage = this.firstPrompt
|
|
86
|
-
? composeInitialAgentPrompt(this.instructions, message)
|
|
87
|
-
: message;
|
|
88
|
-
this.firstPrompt = false;
|
|
89
88
|
const controller = new AbortController();
|
|
90
89
|
this.activeController = controller;
|
|
91
90
|
const timer = setTimeout(() => controller.abort(new Error('A2A message timed out')), Math.min(this.promptTimeoutMs, this.config.timeoutMs || this.promptTimeoutMs));
|
|
92
91
|
timer.unref?.();
|
|
93
92
|
try {
|
|
94
|
-
const request = this.messageRequest(
|
|
93
|
+
const request = this.messageRequest(message, attachments);
|
|
95
94
|
for await (const response of this.client.sendMessageStream(request, {
|
|
96
95
|
signal: controller.signal
|
|
97
96
|
})) {
|
|
98
97
|
this.processResponse(response);
|
|
99
98
|
}
|
|
100
|
-
if (this.sink.state === 'running')
|
|
101
|
-
this.
|
|
99
|
+
if (this.sink.state === 'running') {
|
|
100
|
+
if (this.completionProof) {
|
|
101
|
+
this.sink.completeTurn(this.completionProof);
|
|
102
|
+
}
|
|
103
|
+
else if (!this.sawTaskStatus && this.sawAgentMessage) {
|
|
104
|
+
this.sink.completeTurn({
|
|
105
|
+
source: 'a2a_message_stream',
|
|
106
|
+
stopReason: 'message_stream_end'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.sink.setState('failed', this.sawTaskStatus
|
|
111
|
+
? 'A2A stream ended without a terminal task status.'
|
|
112
|
+
: 'A2A stream ended without an agent result.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
102
115
|
}
|
|
103
116
|
catch (error) {
|
|
104
117
|
if (this.sink.state !== 'canceled') {
|
|
@@ -112,10 +125,19 @@ export class A2AClientRuntime {
|
|
|
112
125
|
this.prompting = false;
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
|
-
async respondPending(
|
|
128
|
+
async respondPending(response, attachments = []) {
|
|
116
129
|
if (this.sink.state !== 'input_required') {
|
|
117
130
|
throw new Error('A2A session is not waiting for input');
|
|
118
131
|
}
|
|
132
|
+
const message = typeof response === 'string'
|
|
133
|
+
? response
|
|
134
|
+
: response.action === 'cancel'
|
|
135
|
+
? 'cancel'
|
|
136
|
+
: response.action === 'decline'
|
|
137
|
+
? 'decline'
|
|
138
|
+
: response.message ?? response.optionId ?? '';
|
|
139
|
+
if (!message.trim())
|
|
140
|
+
throw new Error('A2A pending response is empty');
|
|
119
141
|
this.sink.clearPending();
|
|
120
142
|
await this.prompt(message, attachments);
|
|
121
143
|
}
|
|
@@ -155,6 +177,12 @@ export class A2AClientRuntime {
|
|
|
155
177
|
filename: '',
|
|
156
178
|
mediaType: 'text/plain'
|
|
157
179
|
},
|
|
180
|
+
{
|
|
181
|
+
content: { $case: 'text', value: TURN_COMPLETION_CONTRACT },
|
|
182
|
+
metadata: undefined,
|
|
183
|
+
filename: '',
|
|
184
|
+
mediaType: 'text/plain'
|
|
185
|
+
},
|
|
158
186
|
...attachments.map((attachment) => ({
|
|
159
187
|
content: {
|
|
160
188
|
$case: 'raw',
|
|
@@ -209,6 +237,7 @@ export class A2AClientRuntime {
|
|
|
209
237
|
processMessage(message) {
|
|
210
238
|
if (!message || message.role !== Role.ROLE_AGENT)
|
|
211
239
|
return;
|
|
240
|
+
this.sawAgentMessage = true;
|
|
212
241
|
this.captureIds(message.taskId, message.contextId);
|
|
213
242
|
if (message.messageId && this.emittedMessages.has(message.messageId))
|
|
214
243
|
return;
|
|
@@ -221,10 +250,18 @@ export class A2AClientRuntime {
|
|
|
221
250
|
content: { type: 'text', text }
|
|
222
251
|
});
|
|
223
252
|
}
|
|
253
|
+
message.parts.forEach((part, index) => {
|
|
254
|
+
const artifact = artifactFromPart(part, `message:${message.messageId || 'agent'}:${index}`);
|
|
255
|
+
if (!artifact)
|
|
256
|
+
return;
|
|
257
|
+
this.sink.addArtifact(artifact);
|
|
258
|
+
this.sink.emit('artifact', artifact);
|
|
259
|
+
});
|
|
224
260
|
}
|
|
225
261
|
processStatus(status) {
|
|
226
262
|
if (!status)
|
|
227
263
|
return;
|
|
264
|
+
this.sawTaskStatus = true;
|
|
228
265
|
this.processMessage(status.message);
|
|
229
266
|
const statusText = textParts(status.message?.parts || []).join('\n').trim();
|
|
230
267
|
switch (status.state) {
|
|
@@ -234,7 +271,10 @@ export class A2AClientRuntime {
|
|
|
234
271
|
break;
|
|
235
272
|
case TaskState.TASK_STATE_COMPLETED:
|
|
236
273
|
this.sink.clearPending();
|
|
237
|
-
this.
|
|
274
|
+
this.completionProof = {
|
|
275
|
+
source: 'a2a_task_status',
|
|
276
|
+
stopReason: 'completed'
|
|
277
|
+
};
|
|
238
278
|
break;
|
|
239
279
|
case TaskState.TASK_STATE_CANCELED:
|
|
240
280
|
this.sink.clearPending();
|
|
@@ -348,6 +388,36 @@ function artifactFromA2A(artifact) {
|
|
|
348
388
|
metadata: artifact.metadata || undefined
|
|
349
389
|
};
|
|
350
390
|
}
|
|
391
|
+
function artifactFromPart(part, id) {
|
|
392
|
+
switch (part.content?.$case) {
|
|
393
|
+
case 'raw':
|
|
394
|
+
return {
|
|
395
|
+
id,
|
|
396
|
+
name: part.filename || id,
|
|
397
|
+
filename: part.filename || undefined,
|
|
398
|
+
mediaType: part.mediaType || undefined,
|
|
399
|
+
bytesBase64: Buffer.from(part.content.value).toString('base64')
|
|
400
|
+
};
|
|
401
|
+
case 'url':
|
|
402
|
+
return {
|
|
403
|
+
id,
|
|
404
|
+
name: part.filename || part.content.value || id,
|
|
405
|
+
filename: part.filename || undefined,
|
|
406
|
+
mediaType: part.mediaType || undefined,
|
|
407
|
+
url: part.content.value || undefined
|
|
408
|
+
};
|
|
409
|
+
case 'data':
|
|
410
|
+
return {
|
|
411
|
+
id,
|
|
412
|
+
name: part.filename || id,
|
|
413
|
+
filename: part.filename || undefined,
|
|
414
|
+
mediaType: part.mediaType || undefined,
|
|
415
|
+
data: part.content.value
|
|
416
|
+
};
|
|
417
|
+
default:
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
351
421
|
function textParts(parts) {
|
|
352
422
|
return parts.flatMap((part) => part.content?.$case === 'text' && part.content.value ? [part.content.value] : []);
|
|
353
423
|
}
|
package/dist/acp/runtime.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { AgentDriver } from '../drivers/index.js';
|
|
2
2
|
import type { AcpSessionSink } from '../session-contract.js';
|
|
3
|
-
import type { AgentdInputAttachment } from '../types.js';
|
|
4
|
-
export declare class InvalidPermissionAnswerError extends Error {
|
|
5
|
-
constructor(message: string);
|
|
6
|
-
}
|
|
3
|
+
import type { AgentdInputAttachment, AgentdPendingResponse } from '../types.js';
|
|
7
4
|
export declare class AcpProcessRuntime {
|
|
8
5
|
private readonly driver;
|
|
9
6
|
private readonly sink;
|
|
@@ -15,14 +12,12 @@ export declare class AcpProcessRuntime {
|
|
|
15
12
|
private prompting;
|
|
16
13
|
private readonly maxStderrChunkChars;
|
|
17
14
|
private readonly promptTimeoutMs;
|
|
18
|
-
private readonly instructions;
|
|
19
|
-
private firstPrompt;
|
|
20
15
|
private promptCapabilities;
|
|
21
16
|
private inputDirectory?;
|
|
22
|
-
constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number
|
|
17
|
+
constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number);
|
|
23
18
|
start(workspace: string): Promise<void>;
|
|
24
19
|
prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
25
|
-
respondPending(
|
|
20
|
+
respondPending(input: AgentdPendingResponse | string, _attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
26
21
|
cancel(): Promise<void>;
|
|
27
22
|
dispose(): Promise<void>;
|
|
28
23
|
private requestPermission;
|
package/dist/acp/runtime.js
CHANGED
|
@@ -4,13 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { Readable, Writable } from 'node:stream';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import * as acp from '@agentclientprotocol/sdk';
|
|
7
|
-
import {
|
|
8
|
-
export class InvalidPermissionAnswerError extends Error {
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = 'InvalidPermissionAnswerError';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
7
|
+
import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
|
|
14
8
|
export class AcpProcessRuntime {
|
|
15
9
|
driver;
|
|
16
10
|
sink;
|
|
@@ -22,11 +16,9 @@ export class AcpProcessRuntime {
|
|
|
22
16
|
prompting = false;
|
|
23
17
|
maxStderrChunkChars;
|
|
24
18
|
promptTimeoutMs;
|
|
25
|
-
instructions;
|
|
26
|
-
firstPrompt = true;
|
|
27
19
|
promptCapabilities = {};
|
|
28
20
|
inputDirectory;
|
|
29
|
-
constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000
|
|
21
|
+
constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
|
|
30
22
|
this.driver = driver;
|
|
31
23
|
this.sink = sink;
|
|
32
24
|
const limit = Number.isFinite(maxStderrChunkChars)
|
|
@@ -34,7 +26,6 @@ export class AcpProcessRuntime {
|
|
|
34
26
|
: 16 * 1024;
|
|
35
27
|
this.maxStderrChunkChars = Math.max(1, Math.min(16 * 1024, limit));
|
|
36
28
|
this.promptTimeoutMs = Math.max(10_000, Math.trunc(promptTimeoutMs));
|
|
37
|
-
this.instructions = buildAgentInstructions(customInstructions);
|
|
38
29
|
}
|
|
39
30
|
async start(workspace) {
|
|
40
31
|
if (this.process)
|
|
@@ -70,7 +61,7 @@ export class AcpProcessRuntime {
|
|
|
70
61
|
},
|
|
71
62
|
clientInfo: {
|
|
72
63
|
name: 'nexus-agentd',
|
|
73
|
-
version: '0.
|
|
64
|
+
version: '0.2.0'
|
|
74
65
|
}
|
|
75
66
|
});
|
|
76
67
|
if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
|
|
@@ -96,26 +87,29 @@ export class AcpProcessRuntime {
|
|
|
96
87
|
this.prompting = true;
|
|
97
88
|
this.sink.clearPending();
|
|
98
89
|
this.sink.setState('running');
|
|
99
|
-
const promptMessage = this.firstPrompt
|
|
100
|
-
? composeInitialAgentPrompt(this.instructions, message)
|
|
101
|
-
: message;
|
|
102
|
-
this.firstPrompt = false;
|
|
103
90
|
try {
|
|
104
91
|
const response = await withTimeout(this.connection.agent.request(acp.methods.agent.session.prompt, {
|
|
105
92
|
sessionId,
|
|
106
|
-
prompt: await this.promptBlocks(
|
|
93
|
+
prompt: await this.promptBlocks(message, attachments)
|
|
107
94
|
}), this.promptTimeoutMs, 'ACP prompt timed out');
|
|
108
95
|
if (this.sink.state === 'canceled' || this.sink.state === 'failed') {
|
|
109
96
|
return;
|
|
110
97
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
98
|
+
switch (response.stopReason) {
|
|
99
|
+
case 'end_turn':
|
|
100
|
+
this.sink.completeTurn({
|
|
101
|
+
source: 'acp_prompt_response',
|
|
102
|
+
stopReason: response.stopReason
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
case 'cancelled':
|
|
106
|
+
this.sink.setState('canceled');
|
|
107
|
+
break;
|
|
108
|
+
case 'refusal':
|
|
109
|
+
this.sink.setState('failed', 'ACP agent refused the prompt');
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
this.sink.setState('failed', `ACP turn stopped before completion: ${response.stopReason}`);
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
catch (error) {
|
|
@@ -129,16 +123,47 @@ export class AcpProcessRuntime {
|
|
|
129
123
|
this.prompting = false;
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
|
-
async respondPending(
|
|
126
|
+
async respondPending(input, _attachments = []) {
|
|
127
|
+
const response = typeof input === 'string'
|
|
128
|
+
? {
|
|
129
|
+
requestId: this.pendingInput?.request.id ??
|
|
130
|
+
this.pending?.request.id ??
|
|
131
|
+
'',
|
|
132
|
+
message: input
|
|
133
|
+
}
|
|
134
|
+
: input;
|
|
133
135
|
if (this.pendingInput) {
|
|
134
|
-
this.
|
|
136
|
+
if (response.requestId !== this.pendingInput.request.id) {
|
|
137
|
+
throw new Error(`ACP pending request no longer matches: ${response.requestId}`);
|
|
138
|
+
}
|
|
139
|
+
this.finishInput(response.message ?? response.optionId ?? '', response.action);
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
137
142
|
const pending = this.pending;
|
|
138
143
|
if (!pending)
|
|
139
144
|
throw new Error('ACP session is not waiting for input');
|
|
140
|
-
|
|
145
|
+
if (response.requestId !== pending.request.id) {
|
|
146
|
+
throw new Error(`ACP pending request no longer matches: ${response.requestId}`);
|
|
147
|
+
}
|
|
141
148
|
const options = pending.request.options || [];
|
|
149
|
+
if (response.action === 'cancel' || response.action === 'decline') {
|
|
150
|
+
this.finishPermission({ outcome: { outcome: 'cancelled' } });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (response.action === 'accept') {
|
|
154
|
+
const option = options.find((item) => item.kind?.startsWith('allow')) ||
|
|
155
|
+
options.find((item) => !(item.kind?.startsWith('reject') ?? false));
|
|
156
|
+
if (!option) {
|
|
157
|
+
throw new Error('Permission request does not provide an allow option');
|
|
158
|
+
}
|
|
159
|
+
this.finishPermission({
|
|
160
|
+
outcome: { outcome: 'selected', optionId: option.id }
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const normalized = (response.optionId ?? response.message ?? '')
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase();
|
|
142
167
|
if (['cancel', 'deny', 'reject', '拒绝', '取消', '不同意'].includes(normalized)) {
|
|
143
168
|
this.finishPermission({ outcome: { outcome: 'cancelled' } });
|
|
144
169
|
return;
|
|
@@ -147,9 +172,9 @@ export class AcpProcessRuntime {
|
|
|
147
172
|
const option = Number.isInteger(numeric) && numeric >= 1
|
|
148
173
|
? options[numeric - 1]
|
|
149
174
|
: options.find((item) => item.id.toLowerCase() === normalized ||
|
|
150
|
-
item.name.toLowerCase() === normalized)
|
|
175
|
+
item.name.toLowerCase() === normalized);
|
|
151
176
|
if (!option) {
|
|
152
|
-
throw new
|
|
177
|
+
throw new Error(`Permission answer must be an option id/name or index: ${options
|
|
153
178
|
.map((item, index) => `${index + 1}. ${item.name} (${item.id})`)
|
|
154
179
|
.join('; ')}`);
|
|
155
180
|
}
|
|
@@ -221,18 +246,6 @@ export class AcpProcessRuntime {
|
|
|
221
246
|
}
|
|
222
247
|
: { outcome: { outcome: 'cancelled' } };
|
|
223
248
|
}
|
|
224
|
-
if (this.driver.permissionPolicy === 'allow') {
|
|
225
|
-
const allow = params.options.find((option) => option.kind.toLowerCase() === 'allow_once') ||
|
|
226
|
-
params.options.find((option) => option.kind.toLowerCase().startsWith('allow'));
|
|
227
|
-
return allow
|
|
228
|
-
? {
|
|
229
|
-
outcome: {
|
|
230
|
-
outcome: 'selected',
|
|
231
|
-
optionId: allow.optionId
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
: { outcome: { outcome: 'cancelled' } };
|
|
235
|
-
}
|
|
236
249
|
if (this.pending) {
|
|
237
250
|
return { outcome: { outcome: 'cancelled' } };
|
|
238
251
|
}
|
|
@@ -295,16 +308,17 @@ export class AcpProcessRuntime {
|
|
|
295
308
|
this.pendingInput = { request, params, resolve, timer };
|
|
296
309
|
});
|
|
297
310
|
}
|
|
298
|
-
finishInput(message) {
|
|
311
|
+
finishInput(message, requestedAction) {
|
|
299
312
|
const pending = this.pendingInput;
|
|
300
313
|
if (!pending)
|
|
301
314
|
return;
|
|
302
315
|
const normalized = message.trim().toLowerCase();
|
|
303
316
|
let response;
|
|
304
|
-
if (['cancel', '取消'].includes(normalized)) {
|
|
317
|
+
if (requestedAction === 'cancel' || ['cancel', '取消'].includes(normalized)) {
|
|
305
318
|
response = { action: 'cancel' };
|
|
306
319
|
}
|
|
307
|
-
else if (
|
|
320
|
+
else if (requestedAction === 'decline' ||
|
|
321
|
+
['decline', '拒绝', '不同意'].includes(normalized)) {
|
|
308
322
|
response = { action: 'decline' };
|
|
309
323
|
}
|
|
310
324
|
else if (!isFormElicitation(pending.params)) {
|
|
@@ -434,7 +448,10 @@ export class AcpProcessRuntime {
|
|
|
434
448
|
return value;
|
|
435
449
|
}
|
|
436
450
|
async promptBlocks(message, attachments) {
|
|
437
|
-
const blocks = [
|
|
451
|
+
const blocks = [
|
|
452
|
+
{ type: 'text', text: message },
|
|
453
|
+
{ type: 'text', text: TURN_COMPLETION_CONTRACT }
|
|
454
|
+
];
|
|
438
455
|
for (const attachment of attachments) {
|
|
439
456
|
const mediaType = attachment.mediaType || 'application/octet-stream';
|
|
440
457
|
const data = attachment.bytes.toString('base64');
|
|
@@ -477,39 +494,6 @@ export class AcpProcessRuntime {
|
|
|
477
494
|
};
|
|
478
495
|
}
|
|
479
496
|
}
|
|
480
|
-
function permissionApprovalOption(answer, options) {
|
|
481
|
-
const approvals = new Set([
|
|
482
|
-
'yes',
|
|
483
|
-
'y',
|
|
484
|
-
'ok',
|
|
485
|
-
'okay',
|
|
486
|
-
'approve',
|
|
487
|
-
'approved',
|
|
488
|
-
'accept',
|
|
489
|
-
'allow',
|
|
490
|
-
'proceed',
|
|
491
|
-
'同意',
|
|
492
|
-
'允许',
|
|
493
|
-
'允许一次',
|
|
494
|
-
'确认',
|
|
495
|
-
'好的',
|
|
496
|
-
'可以',
|
|
497
|
-
'继续',
|
|
498
|
-
'是',
|
|
499
|
-
'是的'
|
|
500
|
-
]);
|
|
501
|
-
if (!approvals.has(answer))
|
|
502
|
-
return undefined;
|
|
503
|
-
return (options.find((item) => item.kind?.toLowerCase() === 'allow_once') ||
|
|
504
|
-
options.find((item) => isApprovalOption(item)));
|
|
505
|
-
}
|
|
506
|
-
function isApprovalOption(option) {
|
|
507
|
-
const kind = option.kind?.toLowerCase() || '';
|
|
508
|
-
if (kind.startsWith('allow'))
|
|
509
|
-
return true;
|
|
510
|
-
const values = [option.id, option.name].map((value) => value.trim().toLowerCase());
|
|
511
|
-
return values.some((value) => ['allow', 'approve', 'accept', 'permit', '允许', '同意'].some((word) => value === word || value.startsWith(`${word} `) || value.startsWith(`${word}_`)));
|
|
512
|
-
}
|
|
513
497
|
class PromptTimeoutError extends Error {
|
|
514
498
|
}
|
|
515
499
|
function safeFilename(value) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TURN_COMPLETION_CONTRACT: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const TURN_COMPLETION_CONTRACT = [
|
|
2
|
+
'<agent_nexus_completion_contract>',
|
|
3
|
+
'Do not end this turn while requested work is still running or incomplete.',
|
|
4
|
+
'If you need user input or permission, use the protocol input/permission request and wait for the response.',
|
|
5
|
+
'Before ending the turn, verify the requested work and provide a non-empty final response.',
|
|
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.',
|
|
8
|
+
'</agent_nexus_completion_contract>'
|
|
9
|
+
].join('\n');
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { normalizeAgentInstructions } from './agent-instructions.js';
|
|
4
3
|
import { agentdDriverKinds } from './types.js';
|
|
5
4
|
export async function ensureAgentdConfig(filePath, options = {}) {
|
|
6
5
|
const absolute = path.resolve(filePath);
|
|
@@ -98,12 +97,6 @@ export async function loadAgentdConfig(filePath) {
|
|
|
98
97
|
workspaceRoots,
|
|
99
98
|
maxRequestBytes: integerValue(value.maxRequestBytes, 'maxRequestBytes', 1024 * 1024, 1024, 16 * 1024 * 1024),
|
|
100
99
|
maxAttachmentBytes: integerValue(value.maxAttachmentBytes, 'maxAttachmentBytes', 32 * 1024 * 1024, 1024, 64 * 1024 * 1024),
|
|
101
|
-
artifactStoragePath: resolveConfigPath(absolute, optionalString(value.artifactStoragePath, 'artifactStoragePath') || './artifacts'),
|
|
102
|
-
maxArtifactBytes: integerValue(value.maxArtifactBytes, 'maxArtifactBytes', 512 * 1024 * 1024, 1024, 16 * 1024 * 1024 * 1024),
|
|
103
|
-
maxArtifactStorageBytes: integerValue(value.maxArtifactStorageBytes, 'maxArtifactStorageBytes', 4 * 1024 * 1024 * 1024, 1024 * 1024, Number.MAX_SAFE_INTEGER),
|
|
104
|
-
maxPublishedArtifacts: integerValue(value.maxPublishedArtifacts, 'maxPublishedArtifacts', 4096, 1, 1_000_000),
|
|
105
|
-
maxConcurrentArtifactPublishes: integerValue(value.maxConcurrentArtifactPublishes, 'maxConcurrentArtifactPublishes', 4, 1, 128),
|
|
106
|
-
artifactTtlMs: integerValue(value.artifactTtlMs, 'artifactTtlMs', 24 * 60 * 60 * 1000, 60_000, 30 * 24 * 60 * 60 * 1000),
|
|
107
100
|
maxEventsPerSession: integerValue(value.maxEventsPerSession, 'maxEventsPerSession', 2048, 64, 100_000),
|
|
108
101
|
maxOutputChars: integerValue(value.maxOutputChars, 'maxOutputChars', 512 * 1024, 16 * 1024, 16 * 1024 * 1024),
|
|
109
102
|
sessionTtlMs: integerValue(value.sessionTtlMs, 'sessionTtlMs', 24 * 60 * 60 * 1000, 60_000, 30 * 24 * 60 * 60 * 1000),
|
|
@@ -141,9 +134,7 @@ function parseAgent(id, value) {
|
|
|
141
134
|
throw new Error(`Unsupported nexus-agentd driver: ${driver || id}`);
|
|
142
135
|
}
|
|
143
136
|
const permissionPolicy = optionalString(input.permissionPolicy, `agents.${id}.permissionPolicy`) || 'ask';
|
|
144
|
-
if (permissionPolicy !== 'ask' &&
|
|
145
|
-
permissionPolicy !== 'allow' &&
|
|
146
|
-
permissionPolicy !== 'deny') {
|
|
137
|
+
if (permissionPolicy !== 'ask' && permissionPolicy !== 'deny') {
|
|
147
138
|
throw new Error(`Invalid permissionPolicy for ${id}`);
|
|
148
139
|
}
|
|
149
140
|
const result = {
|
|
@@ -151,7 +142,6 @@ function parseAgent(id, value) {
|
|
|
151
142
|
driver,
|
|
152
143
|
name: optionalString(input.name, `agents.${id}.name`),
|
|
153
144
|
description: optionalString(input.description, `agents.${id}.description`),
|
|
154
|
-
instructions: normalizeAgentInstructions(input.instructions, `agents.${id}.instructions`),
|
|
155
145
|
enabled: optionalBoolean(input.enabled, `agents.${id}.enabled`) ?? true,
|
|
156
146
|
workspace: optionalString(input.workspace, `agents.${id}.workspace`),
|
|
157
147
|
command: optionalString(input.command, `agents.${id}.command`),
|
|
@@ -207,7 +197,6 @@ function parseA2AAgent(id, input) {
|
|
|
207
197
|
protocol: 'a2a',
|
|
208
198
|
name: optionalString(input.name, `agents.${id}.name`),
|
|
209
199
|
description: optionalString(input.description, `agents.${id}.description`),
|
|
210
|
-
instructions: normalizeAgentInstructions(input.instructions, `agents.${id}.instructions`),
|
|
211
200
|
enabled: optionalBoolean(input.enabled, `agents.${id}.enabled`) ?? true,
|
|
212
201
|
agentCardUrl,
|
|
213
202
|
agentUrl,
|
|
@@ -340,9 +329,6 @@ function validateHttpUrl(value, name) {
|
|
|
340
329
|
throw new Error(`nexus-agentd ${name} must be an http(s) URL without credentials or a fragment`);
|
|
341
330
|
}
|
|
342
331
|
}
|
|
343
|
-
function resolveConfigPath(configPath, value) {
|
|
344
|
-
return path.resolve(path.dirname(configPath), value);
|
|
345
|
-
}
|
|
346
332
|
function secretsEqual(left, right) {
|
|
347
333
|
return left.length === right.length && left === right;
|
|
348
334
|
}
|
package/dist/control-plane.d.ts
CHANGED
package/dist/control-plane.js
CHANGED
|
@@ -2,7 +2,6 @@ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { chmod, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { hashAdminPassword, validateAdminPassword, verifyAdminPassword } from './auth.js';
|
|
5
|
-
import { normalizeAgentInstructions } from './agent-instructions.js';
|
|
6
5
|
import { loadAgentdConfig } from './config.js';
|
|
7
6
|
import { createDriverRegistry } from './drivers/index.js';
|
|
8
7
|
import { agentdDriverKinds } from './types.js';
|
|
@@ -312,7 +311,6 @@ export class AgentdControlPlane {
|
|
|
312
311
|
next.name = cleanString(update.name) || cleanString(previous.name);
|
|
313
312
|
}
|
|
314
313
|
}
|
|
315
|
-
setOptionalInstructions(next, update.instructions ?? cleanString(previous.instructions));
|
|
316
314
|
setOptionalString(next, 'description', update.description ?? cleanString(previous.description));
|
|
317
315
|
agents[id] = next;
|
|
318
316
|
raw.agents = agents;
|
|
@@ -450,7 +448,6 @@ export class AgentdControlPlane {
|
|
|
450
448
|
protocol: 'a2a',
|
|
451
449
|
name: config.name || id,
|
|
452
450
|
description: config.description,
|
|
453
|
-
instructions: config.instructions,
|
|
454
451
|
enabled: config.enabled !== false,
|
|
455
452
|
agentCardUrl: config.agentCardUrl || defaultAgentCardUrl(config.agentUrl),
|
|
456
453
|
agentUrl: config.agentUrl,
|
|
@@ -469,7 +466,6 @@ export class AgentdControlPlane {
|
|
|
469
466
|
driver: config.driver,
|
|
470
467
|
name: config.name || id,
|
|
471
468
|
description: config.description,
|
|
472
|
-
instructions: config.instructions,
|
|
473
469
|
enabled: config.enabled !== false,
|
|
474
470
|
workspace: config.workspace || this.config.workspaceRoots[0] || '',
|
|
475
471
|
permissionPolicy: config.permissionPolicy || 'ask',
|
|
@@ -505,9 +501,8 @@ function validateUpdate(update) {
|
|
|
505
501
|
}
|
|
506
502
|
if (update.permissionPolicy !== undefined &&
|
|
507
503
|
update.permissionPolicy !== 'ask' &&
|
|
508
|
-
update.permissionPolicy !== 'allow' &&
|
|
509
504
|
update.permissionPolicy !== 'deny') {
|
|
510
|
-
throw new ControlPlaneError(400, 'permissionPolicy must be ask
|
|
505
|
+
throw new ControlPlaneError(400, 'permissionPolicy must be ask or deny');
|
|
511
506
|
}
|
|
512
507
|
if (update.preferredTransport !== undefined &&
|
|
513
508
|
!['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
|
|
@@ -612,19 +607,6 @@ function setOptionalString(target, key, value) {
|
|
|
612
607
|
else
|
|
613
608
|
delete target[key];
|
|
614
609
|
}
|
|
615
|
-
function setOptionalInstructions(target, value) {
|
|
616
|
-
let text;
|
|
617
|
-
try {
|
|
618
|
-
text = normalizeAgentInstructions(value);
|
|
619
|
-
}
|
|
620
|
-
catch (error) {
|
|
621
|
-
throw new ControlPlaneError(400, errorMessage(error));
|
|
622
|
-
}
|
|
623
|
-
if (text)
|
|
624
|
-
target.instructions = text;
|
|
625
|
-
else
|
|
626
|
-
delete target.instructions;
|
|
627
|
-
}
|
|
628
610
|
function validHeaderName(value) {
|
|
629
611
|
return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);
|
|
630
612
|
}
|
package/dist/run-store.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare class RunStore {
|
|
|
32
32
|
constructor(filePath: string, maxRuns?: number, maxTaskChars?: number);
|
|
33
33
|
init(): Promise<void>;
|
|
34
34
|
create(input: CreateRunInput): AgentdRunDetail;
|
|
35
|
-
update(id: string, patch: Partial<Pick<StoredRun, 'protocolSessionId' | 'state' | 'progress' | 'output' | 'error' | 'artifacts' | 'artifactCount' | 'updatedAt' | 'endedAt' | 'durationMs' | 'inputAttachmentCount'>>): AgentdRunDetail | undefined;
|
|
35
|
+
update(id: string, patch: Partial<Pick<StoredRun, 'protocolSessionId' | 'state' | 'progress' | 'output' | 'error' | 'artifacts' | 'artifactCount' | 'updatedAt' | 'endedAt' | 'durationMs' | 'inputAttachmentCount' | 'completion'>>): AgentdRunDetail | undefined;
|
|
36
36
|
get(id: string): AgentdRunDetail | undefined;
|
|
37
37
|
list(query?: RunListQuery): {
|
|
38
38
|
runs: AgentdRunView[];
|