nexus-agentd 0.1.8 → 0.1.9

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,8 +10,6 @@ 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;
@@ -19,7 +17,7 @@ export declare class A2AClientRuntime implements AgentSessionRuntime {
19
17
  constructor(config: AgentdA2AConfig, sink: AgentSessionSink, promptTimeoutMs: number);
20
18
  start(): Promise<void>;
21
19
  prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
22
- respondPending(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
20
+ respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
23
21
  cancel(): Promise<void>;
24
22
  dispose(): Promise<void>;
25
23
  private messageRequest;
@@ -1,7 +1,6 @@
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';
5
4
  export async function probeA2AAgent(id, config) {
6
5
  const startedAt = Date.now();
7
6
  if (config.enabled === false)
@@ -44,8 +43,6 @@ export class A2AClientRuntime {
44
43
  taskId = '';
45
44
  contextId = '';
46
45
  prompting = false;
47
- firstPrompt = true;
48
- instructions;
49
46
  disposed = false;
50
47
  activeController;
51
48
  artifactCache = new Map();
@@ -54,7 +51,6 @@ export class A2AClientRuntime {
54
51
  this.config = config;
55
52
  this.sink = sink;
56
53
  this.promptTimeoutMs = promptTimeoutMs;
57
- this.instructions = buildAgentInstructions(config.instructions);
58
54
  }
59
55
  async start() {
60
56
  const resolver = createResolver(this.config);
@@ -82,16 +78,12 @@ export class A2AClientRuntime {
82
78
  this.prompting = true;
83
79
  this.sink.clearPending();
84
80
  this.sink.setState('running');
85
- const promptMessage = this.firstPrompt
86
- ? composeInitialAgentPrompt(this.instructions, message)
87
- : message;
88
- this.firstPrompt = false;
89
81
  const controller = new AbortController();
90
82
  this.activeController = controller;
91
83
  const timer = setTimeout(() => controller.abort(new Error('A2A message timed out')), Math.min(this.promptTimeoutMs, this.config.timeoutMs || this.promptTimeoutMs));
92
84
  timer.unref?.();
93
85
  try {
94
- const request = this.messageRequest(promptMessage, attachments);
86
+ const request = this.messageRequest(message, attachments);
95
87
  for await (const response of this.client.sendMessageStream(request, {
96
88
  signal: controller.signal
97
89
  })) {
@@ -112,10 +104,19 @@ export class A2AClientRuntime {
112
104
  this.prompting = false;
113
105
  }
114
106
  }
115
- async respondPending(message, attachments = []) {
107
+ async respondPending(response, attachments = []) {
116
108
  if (this.sink.state !== 'input_required') {
117
109
  throw new Error('A2A session is not waiting for input');
118
110
  }
111
+ const message = typeof response === 'string'
112
+ ? response
113
+ : response.action === 'cancel'
114
+ ? 'cancel'
115
+ : response.action === 'decline'
116
+ ? 'decline'
117
+ : response.message ?? response.optionId ?? '';
118
+ if (!message.trim())
119
+ throw new Error('A2A pending response is empty');
119
120
  this.sink.clearPending();
120
121
  await this.prompt(message, attachments);
121
122
  }
@@ -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,6 @@ 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
- }
14
7
  export class AcpProcessRuntime {
15
8
  driver;
16
9
  sink;
@@ -22,11 +15,9 @@ export class AcpProcessRuntime {
22
15
  prompting = false;
23
16
  maxStderrChunkChars;
24
17
  promptTimeoutMs;
25
- instructions;
26
- firstPrompt = true;
27
18
  promptCapabilities = {};
28
19
  inputDirectory;
29
- constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000, customInstructions) {
20
+ constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
30
21
  this.driver = driver;
31
22
  this.sink = sink;
32
23
  const limit = Number.isFinite(maxStderrChunkChars)
@@ -34,7 +25,6 @@ export class AcpProcessRuntime {
34
25
  : 16 * 1024;
35
26
  this.maxStderrChunkChars = Math.max(1, Math.min(16 * 1024, limit));
36
27
  this.promptTimeoutMs = Math.max(10_000, Math.trunc(promptTimeoutMs));
37
- this.instructions = buildAgentInstructions(customInstructions);
38
28
  }
39
29
  async start(workspace) {
40
30
  if (this.process)
@@ -96,14 +86,10 @@ export class AcpProcessRuntime {
96
86
  this.prompting = true;
97
87
  this.sink.clearPending();
98
88
  this.sink.setState('running');
99
- const promptMessage = this.firstPrompt
100
- ? composeInitialAgentPrompt(this.instructions, message)
101
- : message;
102
- this.firstPrompt = false;
103
89
  try {
104
90
  const response = await withTimeout(this.connection.agent.request(acp.methods.agent.session.prompt, {
105
91
  sessionId,
106
- prompt: await this.promptBlocks(promptMessage, attachments)
92
+ prompt: await this.promptBlocks(message, attachments)
107
93
  }), this.promptTimeoutMs, 'ACP prompt timed out');
108
94
  if (this.sink.state === 'canceled' || this.sink.state === 'failed') {
109
95
  return;
@@ -129,16 +115,47 @@ export class AcpProcessRuntime {
129
115
  this.prompting = false;
130
116
  }
131
117
  }
132
- async respondPending(message, _attachments = []) {
118
+ async respondPending(input, _attachments = []) {
119
+ const response = typeof input === 'string'
120
+ ? {
121
+ requestId: this.pendingInput?.request.id ??
122
+ this.pending?.request.id ??
123
+ '',
124
+ message: input
125
+ }
126
+ : input;
133
127
  if (this.pendingInput) {
134
- this.finishInput(message);
128
+ if (response.requestId !== this.pendingInput.request.id) {
129
+ throw new Error(`ACP pending request no longer matches: ${response.requestId}`);
130
+ }
131
+ this.finishInput(response.message ?? response.optionId ?? '', response.action);
135
132
  return;
136
133
  }
137
134
  const pending = this.pending;
138
135
  if (!pending)
139
136
  throw new Error('ACP session is not waiting for input');
140
- const normalized = message.trim().toLowerCase();
137
+ if (response.requestId !== pending.request.id) {
138
+ throw new Error(`ACP pending request no longer matches: ${response.requestId}`);
139
+ }
141
140
  const options = pending.request.options || [];
141
+ if (response.action === 'cancel' || response.action === 'decline') {
142
+ this.finishPermission({ outcome: { outcome: 'cancelled' } });
143
+ return;
144
+ }
145
+ if (response.action === 'accept') {
146
+ const option = options.find((item) => item.kind?.startsWith('allow')) ||
147
+ options.find((item) => !(item.kind?.startsWith('reject') ?? false));
148
+ if (!option) {
149
+ throw new Error('Permission request does not provide an allow option');
150
+ }
151
+ this.finishPermission({
152
+ outcome: { outcome: 'selected', optionId: option.id }
153
+ });
154
+ return;
155
+ }
156
+ const normalized = (response.optionId ?? response.message ?? '')
157
+ .trim()
158
+ .toLowerCase();
142
159
  if (['cancel', 'deny', 'reject', '拒绝', '取消', '不同意'].includes(normalized)) {
143
160
  this.finishPermission({ outcome: { outcome: 'cancelled' } });
144
161
  return;
@@ -147,9 +164,9 @@ export class AcpProcessRuntime {
147
164
  const option = Number.isInteger(numeric) && numeric >= 1
148
165
  ? options[numeric - 1]
149
166
  : options.find((item) => item.id.toLowerCase() === normalized ||
150
- item.name.toLowerCase() === normalized) || permissionApprovalOption(normalized, options);
167
+ item.name.toLowerCase() === normalized);
151
168
  if (!option) {
152
- throw new InvalidPermissionAnswerError(`Permission answer must be an option id/name or index: ${options
169
+ throw new Error(`Permission answer must be an option id/name or index: ${options
153
170
  .map((item, index) => `${index + 1}. ${item.name} (${item.id})`)
154
171
  .join('; ')}`);
155
172
  }
@@ -221,18 +238,6 @@ export class AcpProcessRuntime {
221
238
  }
222
239
  : { outcome: { outcome: 'cancelled' } };
223
240
  }
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
241
  if (this.pending) {
237
242
  return { outcome: { outcome: 'cancelled' } };
238
243
  }
@@ -295,16 +300,17 @@ export class AcpProcessRuntime {
295
300
  this.pendingInput = { request, params, resolve, timer };
296
301
  });
297
302
  }
298
- finishInput(message) {
303
+ finishInput(message, requestedAction) {
299
304
  const pending = this.pendingInput;
300
305
  if (!pending)
301
306
  return;
302
307
  const normalized = message.trim().toLowerCase();
303
308
  let response;
304
- if (['cancel', '取消'].includes(normalized)) {
309
+ if (requestedAction === 'cancel' || ['cancel', '取消'].includes(normalized)) {
305
310
  response = { action: 'cancel' };
306
311
  }
307
- else if (['decline', '拒绝', '不同意'].includes(normalized)) {
312
+ else if (requestedAction === 'decline' ||
313
+ ['decline', '拒绝', '不同意'].includes(normalized)) {
308
314
  response = { action: 'decline' };
309
315
  }
310
316
  else if (!isFormElicitation(pending.params)) {
@@ -477,39 +483,6 @@ export class AcpProcessRuntime {
477
483
  };
478
484
  }
479
485
  }
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
486
  class PromptTimeoutError extends Error {
514
487
  }
515
488
  function safeFilename(value) {
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
  }