@zvada/agent-server 0.3.2 → 0.3.4

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.
@@ -111,16 +111,31 @@ export class AgentServer {
111
111
  if (this.shuttingDown) return;
112
112
  this.shuttingDown = true;
113
113
  // Drain first so cancelled turns still broadcast their turn.ended.
114
- await this.runtime.shutdown();
115
- for (const transport of [...this.transports]) transport.close();
116
- this.options.onShutdown?.();
114
+ // The drain (and each transport's close) is a library extension point
115
+ // that may throw; the finally still closes every transport and runs the
116
+ // host hook even then — every other external call in this file is
117
+ // defended the same way (broadcast/observers/sink/handleLine).
118
+ try {
119
+ await this.runtime.shutdown();
120
+ } finally {
121
+ for (const transport of [...this.transports]) {
122
+ try {
123
+ transport.close();
124
+ } catch {
125
+ // One faulty transport's close must not skip the others or the
126
+ // host's shutdown hook — same isolation broadcast uses for send.
127
+ }
128
+ }
129
+ this.options.onShutdown?.();
130
+ }
117
131
  }
118
132
 
119
133
  private async handleLine(transport: WireTransport, line: string): Promise<void> {
120
134
  if (!line.trim()) return;
121
135
  const msg = decodeWireMessage(line);
122
136
  if (msg.kind === "invalid") {
123
- transport.send(encodeErrorResponse(null, WIRE_ERROR_CODES.parseError, msg.error));
137
+ const code = msg.parseError ? WIRE_ERROR_CODES.parseError : WIRE_ERROR_CODES.invalidRequest;
138
+ transport.send(encodeErrorResponse(null, code, msg.error));
124
139
  return;
125
140
  }
126
141
  // Clients only send requests on this wire; stray notifications/responses
@@ -375,7 +390,10 @@ export class AgentServer {
375
390
 
376
391
  case WIRE_METHODS.shutdown: {
377
392
  transport.send(encodeResponse(id, {}));
378
- await this.shutdown();
393
+ // The success ack is already committed on the wire; a throw out of
394
+ // shutdown() must not reach handleLine's catch and emit a second
395
+ // response for this id (a JSON-RPC 2.0 violation).
396
+ await this.shutdown().catch(() => {});
379
397
  return;
380
398
  }
381
399
 
package/src/server/bin.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  // agent-server install # prefetch the pinned agent CLIs
12
12
  //
13
13
  // All logs go to stderr; stdout is reserved for the wire in stdio mode.
14
+ import pkg from "../../package.json" with { type: "json" };
14
15
  import {
15
16
  KNOWN_ACP_AGENTS,
16
17
  type ProvisionMode,
@@ -19,13 +20,14 @@ import {
19
20
  } from "../core/index.ts";
20
21
  import { isAgentHarness } from "../protocol/index.ts";
21
22
  import { AgentServer } from "./agent-server.ts";
23
+ import { runFeedback } from "./feedback.ts";
22
24
  import { runInstall } from "./install.ts";
23
25
  import { dial, listen, stdioTransport } from "./transports.ts";
24
26
 
25
- const VERSION = "0.1.0";
27
+ const VERSION: string = pkg.version;
26
28
 
27
29
  interface Args {
28
- command: "serve" | "install";
30
+ command: "serve" | "install" | "feedback";
29
31
  mode: "stdio" | "listen" | "dial" | "acp";
30
32
  listen?: { hostname?: string; port: number };
31
33
  dialUrl?: string;
@@ -33,21 +35,27 @@ interface Args {
33
35
  acpAgent?: string;
34
36
  bufferSize?: number;
35
37
  provisionMode?: ProvisionMode;
38
+ feedback?: { message?: string; category?: string; subject?: string; resume?: string };
36
39
  }
37
40
 
38
41
  function usage(exitCode: number): never {
39
42
  console.error(
40
43
  [
41
- "usage: agent-server [install] [--listen [host:]port | --dial <ws-url>] [options]",
44
+ "usage: agent-server [install|feedback] [--listen [host:]port | --dial <ws-url>] [options]",
42
45
  "",
43
46
  " (default) serve NDJSON JSON-RPC on stdio",
44
47
  " --acp speak ACP v1 on stdio instead (be the agent)",
45
48
  " install download the pinned agent CLIs into the managed cache",
46
49
  " (always downloads — run at image build time to erase",
47
50
  " sandbox cold starts; --provision applies to serving only)",
51
+ ' feedback "<report>" report friction to the agent-server maintainers',
52
+ " (--category api|cli|docs|other, --subject <exact item>,",
53
+ " --resume <threadId>; no auth; structured eval reports",
54
+ " ride `npx hivenet --to agent-server --task ...`)",
48
55
  " --listen [host:]port accept WebSocket clients",
49
56
  " --dial <ws-url> dial out to a consumer (auto-reconnect)",
50
57
  " --harness <a,b> restrict harnesses (default: all installed)",
58
+ " --acp-agent <spec> ACP agent: known name (pi|gemini) or launch command",
51
59
  " --provision <mode> CLI provisioning: auto | pinned | system (default auto)",
52
60
  " --buffer <n> replay buffer size per session (default 4096)",
53
61
  " --version, --help",
@@ -69,7 +77,17 @@ function parseArgs(argv: string[]): Args {
69
77
  return v;
70
78
  };
71
79
  if (arg === "install" && i === 0) args.command = "install";
72
- else if (arg === "--stdio") args.mode = "stdio";
80
+ else if (arg === "feedback" && i === 0) {
81
+ args.command = "feedback";
82
+ args.feedback = {};
83
+ } else if (args.command === "feedback" && args.feedback) {
84
+ if (arg === "--category") args.feedback.category = next();
85
+ else if (arg === "--subject") args.feedback.subject = next();
86
+ else if (arg === "--resume") args.feedback.resume = next();
87
+ else if (!arg.startsWith("--") && args.feedback.message === undefined)
88
+ args.feedback.message = arg;
89
+ else usage(1);
90
+ } else if (arg === "--stdio") args.mode = "stdio";
73
91
  else if (arg === "--acp") args.mode = "acp";
74
92
  else if (arg === "--listen") {
75
93
  args.mode = "listen";
@@ -80,7 +98,11 @@ function parseArgs(argv: string[]): Args {
80
98
  args.mode = "dial";
81
99
  args.dialUrl = next();
82
100
  } else if (arg === "--harness") args.harnesses = next().split(",").filter(Boolean);
83
- else if (arg === "--provision") {
101
+ else if (arg === "--acp-agent" || arg.startsWith("--acp-agent=")) {
102
+ const spec = arg === "--acp-agent" ? next() : arg.slice(12);
103
+ if (!spec.trim() || spec.startsWith("-")) usage(1);
104
+ args.acpAgent = spec;
105
+ } else if (arg === "--provision") {
84
106
  const mode = next();
85
107
  if (mode !== "auto" && mode !== "pinned" && mode !== "system") usage(1);
86
108
  args.provisionMode = mode;
@@ -98,6 +120,19 @@ function parseArgs(argv: string[]): Args {
98
120
 
99
121
  const args = parseArgs(process.argv.slice(2));
100
122
 
123
+ if (args.command === "feedback") {
124
+ const message = args.feedback?.message;
125
+ if (!message) usage(1);
126
+ try {
127
+ const result = await runFeedback({ ...args.feedback, message, clientVersion: VERSION });
128
+ for (const line of result.lines) console.error(line);
129
+ process.exit(0);
130
+ } catch (error) {
131
+ console.error(`feedback failed: ${error instanceof Error ? error.message : String(error)}`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+
101
136
  const harnesses = args.harnesses?.map((h) => {
102
137
  if (isAgentHarness(h)) return h;
103
138
  if (KNOWN_ACP_AGENTS[h]) {
@@ -0,0 +1,76 @@
1
+ // `agent-server feedback` — report friction to the maintainers over hivenet.
2
+ //
3
+ // Deliberately zero-dep (raw fetch, no `hivenet` package): the compiled
4
+ // single-binary deploys have no npm/npx, so this subcommand is the only
5
+ // feedback path there. Wire shape per hivenet's v1 feedback API.
6
+
7
+ const ENDPOINT = "https://hivenet.app/v1/feedback";
8
+ const PROJECT = "agent-server";
9
+ const TIMEOUT_MS = 10_000;
10
+
11
+ export interface FeedbackOptions {
12
+ message: string;
13
+ /** Suggested: api | cli | docs | other. Defaults to cli. */
14
+ category?: string;
15
+ /** The exact item: export, error code, harness, or command. */
16
+ subject?: string;
17
+ /** Continue an existing thread instead of opening a new one. */
18
+ resume?: string;
19
+ clientVersion?: string;
20
+ /** Injectable transport boundary for tests. */
21
+ fetchImpl?: (url: string, init: RequestInit) => Promise<Response>;
22
+ }
23
+
24
+ export interface FeedbackResult {
25
+ threadId: string;
26
+ /** Human-readable lines to print (thread id, guidance, ask). */
27
+ lines: string[];
28
+ }
29
+
30
+ /** POST one feedback report; returns the lines the CLI should print. */
31
+ export async function runFeedback(options: FeedbackOptions): Promise<FeedbackResult> {
32
+ const doFetch = options.fetchImpl ?? fetch;
33
+ const threadId = options.resume ?? crypto.randomUUID().replaceAll("-", "");
34
+ const response = await doFetch(ENDPOINT, {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify({
38
+ v: 1,
39
+ to: PROJECT,
40
+ feedback: options.message,
41
+ category: options.category ?? "cli",
42
+ ...(options.subject ? { subject: options.subject } : {}),
43
+ thread: { id: threadId },
44
+ client: { name: "agent-server-cli", version: options.clientVersion ?? "0" },
45
+ consent: { telemetry: false },
46
+ }),
47
+ signal: AbortSignal.timeout(TIMEOUT_MS),
48
+ });
49
+ if (!response.ok) {
50
+ throw new Error(`feedback rejected: ${response.status} ${await safeText(response)}`);
51
+ }
52
+ const body = (await response.json()) as {
53
+ thread?: string;
54
+ guidance?: string;
55
+ ask?: { prompt?: string; command?: string };
56
+ };
57
+ const thread = body.thread ?? threadId;
58
+ const lines = [
59
+ `delivered to ${PROJECT} (thread ${thread})`,
60
+ `continue this thread: agent-server feedback --resume ${thread} "<follow-up>"`,
61
+ ];
62
+ if (body.guidance) lines.push(`guidance from the maintainers: ${body.guidance}`);
63
+ if (body.ask?.prompt) {
64
+ lines.push(`the maintainers ask: ${body.ask.prompt}`);
65
+ if (body.ask.command) lines.push(` answer (only if grounded): ${body.ask.command}`);
66
+ }
67
+ return { threadId: thread, lines };
68
+ }
69
+
70
+ async function safeText(response: Response): Promise<string> {
71
+ try {
72
+ return (await response.text()).slice(0, 200);
73
+ } catch {
74
+ return "";
75
+ }
76
+ }