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.
@@ -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(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
23
+ respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
23
24
  cancel(): Promise<void>;
24
25
  dispose(): Promise<void>;
25
26
  private messageRequest;
@@ -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 { buildAgentInstructions, composeInitialAgentPrompt } from '../agent-instructions.js';
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(promptMessage, attachments);
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.sink.setState('completed');
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(message, attachments = []) {
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.sink.setState('completed');
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
  }
@@ -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, customInstructions?: string);
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(message: string, _attachments?: AgentdInputAttachment[]): Promise<void>;
20
+ respondPending(input: AgentdPendingResponse | string, _attachments?: AgentdInputAttachment[]): Promise<void>;
26
21
  cancel(): Promise<void>;
27
22
  dispose(): Promise<void>;
28
23
  private requestPermission;
@@ -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 { buildAgentInstructions, composeInitialAgentPrompt } from '../agent-instructions.js';
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, customInstructions) {
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.1.4'
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(promptMessage, attachments)
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
- if (response.stopReason === 'cancelled') {
112
- this.sink.setState('canceled');
113
- }
114
- else if (response.stopReason === 'refusal') {
115
- this.sink.setState('failed', 'ACP agent refused the prompt');
116
- }
117
- else {
118
- this.sink.setState('completed');
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(message, _attachments = []) {
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.finishInput(message);
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
- const normalized = message.trim().toLowerCase();
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) || permissionApprovalOption(normalized, options);
175
+ item.name.toLowerCase() === normalized);
151
176
  if (!option) {
152
- throw new InvalidPermissionAnswerError(`Permission answer must be an option id/name or index: ${options
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 (['decline', '拒绝', '不同意'].includes(normalized)) {
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 = [{ type: 'text', text: message }];
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
  }
@@ -5,7 +5,6 @@ export interface AgentdAgentUpdate {
5
5
  driver?: AgentdDriverKind;
6
6
  name?: string;
7
7
  description?: string;
8
- instructions?: string;
9
8
  enabled?: boolean;
10
9
  workspace?: string;
11
10
  permissionPolicy?: PermissionPolicy;
@@ -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, allow, or deny');
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
  }
@@ -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[];