@smithers-orchestrator/agents 0.20.4 → 0.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.20.4",
3
+ "version": "0.22.0",
4
4
  "description": "AI SDK and CLI agent adapters for Smithers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -50,9 +50,9 @@
50
50
  "ai": "^6.0.168",
51
51
  "effect": "^3.21.1",
52
52
  "zod": "^4.3.6",
53
- "@smithers-orchestrator/errors": "0.20.4",
54
- "@smithers-orchestrator/driver": "0.20.4",
55
- "@smithers-orchestrator/observability": "0.20.4"
53
+ "@smithers-orchestrator/driver": "0.22.0",
54
+ "@smithers-orchestrator/errors": "0.22.0",
55
+ "@smithers-orchestrator/observability": "0.22.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/bun": "latest",
@@ -60,7 +60,7 @@
60
60
  "typescript": "~5.9.3"
61
61
  },
62
62
  "scripts": {
63
- "test": "bun test tests",
63
+ "test": "bun test --timeout=60000 --max-concurrency=1 tests",
64
64
  "typecheck": "tsc -p tsconfig.json --noEmit",
65
65
  "build": "tsup --dts-only"
66
66
  }
@@ -0,0 +1,267 @@
1
+ import { BaseCliAgent, pushFlag, pushList, isRecord, asString, truncate, toolKindFromName, createSyntheticIdGenerator, } from "./BaseCliAgent/index.js";
2
+ import { normalizeCapabilityStringList, } from "./capability-registry/index.js";
3
+ /** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
4
+ /** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
5
+ /** @typedef {import("./AntigravityAgentOptions.ts").AntigravityAgentOptions} AntigravityAgentOptions */
6
+
7
+ /**
8
+ * @param {AntigravityAgentOptions} opts
9
+ */
10
+ function resolveAntigravityBuiltIns(opts) {
11
+ return opts.allowedTools?.length
12
+ ? normalizeCapabilityStringList(opts.allowedTools)
13
+ : ["default"];
14
+ }
15
+
16
+ /**
17
+ * @param {AntigravityAgentOptions} [opts]
18
+ * @returns {AgentCapabilityRegistry}
19
+ */
20
+ export function createAntigravityCapabilityRegistry(opts = {}) {
21
+ return {
22
+ version: 1,
23
+ engine: "antigravity",
24
+ runtimeTools: {},
25
+ mcp: {
26
+ bootstrap: "project-config",
27
+ supportsProjectScope: true,
28
+ supportsUserScope: true,
29
+ },
30
+ skills: {
31
+ supportsSkills: true,
32
+ installMode: "plugin",
33
+ smithersSkillIds: [],
34
+ },
35
+ humanInteraction: {
36
+ supportsUiRequests: false,
37
+ methods: [],
38
+ },
39
+ builtIns: resolveAntigravityBuiltIns(opts),
40
+ };
41
+ }
42
+
43
+ export class AntigravityAgent extends BaseCliAgent {
44
+ opts;
45
+ capabilities;
46
+ cliEngine = "antigravity";
47
+ /**
48
+ * @param {AntigravityAgentOptions} [opts]
49
+ */
50
+ constructor(opts = {}) {
51
+ super(opts);
52
+ this.opts = opts;
53
+ this.capabilities = createAntigravityCapabilityRegistry(opts);
54
+ }
55
+ /**
56
+ * @returns {CliOutputInterpreter}
57
+ */
58
+ createOutputInterpreter() {
59
+ let sessionId;
60
+ let finalAnswer = "";
61
+ let didEmitCompleted = false;
62
+ const nextSyntheticId = createSyntheticIdGenerator();
63
+ /**
64
+ * @param {string} line
65
+ * @returns {AgentCliEvent[]}
66
+ */
67
+ const parseLine = (line) => {
68
+ const trimmed = line.trim();
69
+ if (!trimmed)
70
+ return [];
71
+ let payload;
72
+ try {
73
+ payload = JSON.parse(trimmed);
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ if (!isRecord(payload))
79
+ return [];
80
+ const type = asString(payload.type);
81
+ if (!type)
82
+ return [];
83
+ if (type === "init") {
84
+ const resume = asString(payload.session_id);
85
+ if (resume) {
86
+ sessionId = resume;
87
+ }
88
+ return [{
89
+ type: "started",
90
+ engine: this.cliEngine,
91
+ title: "Antigravity CLI",
92
+ resume: sessionId,
93
+ detail: {
94
+ model: asString(payload.model),
95
+ },
96
+ }];
97
+ }
98
+ if (type === "MESSAGE") {
99
+ const role = asString(payload.role);
100
+ const content = asString(payload.content);
101
+ if (role === "assistant" && content) {
102
+ if (payload.delta === true) {
103
+ finalAnswer += content;
104
+ }
105
+ else {
106
+ finalAnswer = content;
107
+ }
108
+ }
109
+ return [];
110
+ }
111
+ if (type === "TOOL_USE") {
112
+ const toolName = asString(payload.tool_name) ?? "tool";
113
+ const toolId = asString(payload.tool_id) ?? nextSyntheticId("antigravity-tool");
114
+ return [{
115
+ type: "action",
116
+ engine: this.cliEngine,
117
+ phase: "started",
118
+ entryType: "thought",
119
+ action: {
120
+ id: toolId,
121
+ kind: toolKindFromName(toolName),
122
+ title: toolName,
123
+ detail: {
124
+ parameters: payload.parameters,
125
+ },
126
+ },
127
+ message: `Running ${toolName}`,
128
+ level: "info",
129
+ }];
130
+ }
131
+ if (type === "TOOL_RESULT") {
132
+ const toolId = asString(payload.tool_id) ?? nextSyntheticId("antigravity-tool");
133
+ const ok = asString(payload.status) !== "error";
134
+ const error = isRecord(payload.error) ? asString(payload.error.message) : undefined;
135
+ const output = asString(payload.output);
136
+ return [{
137
+ type: "action",
138
+ engine: this.cliEngine,
139
+ phase: "completed",
140
+ entryType: "thought",
141
+ action: {
142
+ id: toolId,
143
+ kind: "tool",
144
+ title: "tool result",
145
+ detail: {
146
+ status: asString(payload.status),
147
+ output: output ? truncate(output, 400) : undefined,
148
+ },
149
+ },
150
+ message: error ?? output,
151
+ ok,
152
+ level: ok ? "info" : "warning",
153
+ }];
154
+ }
155
+ if (type === "ERROR") {
156
+ return [{
157
+ type: "action",
158
+ engine: this.cliEngine,
159
+ phase: "completed",
160
+ entryType: "thought",
161
+ action: {
162
+ id: nextSyntheticId("antigravity-warning"),
163
+ kind: "warning",
164
+ title: "warning",
165
+ detail: {
166
+ severity: asString(payload.severity),
167
+ },
168
+ },
169
+ message: asString(payload.message),
170
+ ok: asString(payload.severity) !== "error",
171
+ level: asString(payload.severity) === "error" ? "error" : "warning",
172
+ }];
173
+ }
174
+ if (type === "RESULT") {
175
+ if (didEmitCompleted)
176
+ return [];
177
+ didEmitCompleted = true;
178
+ return [{
179
+ type: "completed",
180
+ engine: this.cliEngine,
181
+ ok: asString(payload.status) !== "error",
182
+ answer: finalAnswer || asString(payload.response),
183
+ resume: sessionId,
184
+ usage: isRecord(payload.stats) ? payload.stats : undefined,
185
+ }];
186
+ }
187
+ return [];
188
+ };
189
+ return {
190
+ onStdoutLine: parseLine,
191
+ onExit: (result) => {
192
+ if (didEmitCompleted)
193
+ return [];
194
+ if (result.exitCode === 0)
195
+ return [];
196
+ didEmitCompleted = true;
197
+ return [{
198
+ type: "completed",
199
+ engine: this.cliEngine,
200
+ ok: false,
201
+ answer: finalAnswer || undefined,
202
+ error: result.stderr.trim() || `Antigravity exited with code ${result.exitCode}`,
203
+ resume: sessionId,
204
+ }];
205
+ },
206
+ };
207
+ }
208
+ /**
209
+ * @param {{ prompt: string; systemPrompt?: string; cwd: string; options: any; }} params
210
+ */
211
+ async buildCommand(params) {
212
+ const args = [];
213
+ const yoloEnabled = this.opts.dangerouslySkipPermissions ?? this.opts.yolo ?? this.yolo;
214
+ const outputFormat = this.opts.outputFormat ??
215
+ (params.options?.onEvent ? "stream-json" : "json");
216
+ const resumeSession = typeof params.options?.resumeSession === "string"
217
+ ? params.options.resumeSession
218
+ : this.opts.resume;
219
+ if (this.opts.debug)
220
+ args.push("--debug");
221
+ pushFlag(args, "--model", this.opts.model ?? this.model);
222
+ if (this.opts.sandbox)
223
+ args.push("--sandbox");
224
+ if (yoloEnabled)
225
+ args.push("--dangerously-skip-permissions");
226
+ pushList(args, "--allowed-mcp-server-names", this.opts.allowedMcpServerNames);
227
+ if (this.opts.allowedTools !== undefined) {
228
+ if (this.opts.allowedTools.length === 0) {
229
+ pushFlag(args, "--allowed-tools", "");
230
+ }
231
+ else {
232
+ pushList(args, "--allowed-tools", this.opts.allowedTools);
233
+ }
234
+ }
235
+ pushList(args, "--extensions", this.opts.extensions);
236
+ if (this.opts.listExtensions)
237
+ args.push("--list-extensions");
238
+ pushFlag(args, "--resume", resumeSession);
239
+ if (this.opts.listSessions)
240
+ args.push("--list-sessions");
241
+ pushFlag(args, "--delete-session", this.opts.deleteSession);
242
+ pushList(args, "--include-directories", this.opts.includeDirectories);
243
+ if (this.opts.screenReader)
244
+ args.push("--screen-reader");
245
+ pushFlag(args, "--gemini_dir", this.opts.geminiDir ?? this.opts.configDir);
246
+ pushFlag(args, "--output-format", outputFormat);
247
+ if (this.extraArgs?.length)
248
+ args.push(...this.extraArgs);
249
+ const systemPrefix = params.systemPrompt
250
+ ? `${params.systemPrompt}\n\n`
251
+ : "";
252
+ const jsonReminder = params.prompt?.includes("REQUIRED OUTPUT")
253
+ ? "\n\nREMINDER: Your response MUST be ONLY the required raw JSON object. Do not include prose, markdown, or code fences. The first character must be `{` and the last character must be `}`.\n"
254
+ : "";
255
+ const fullPrompt = `${systemPrefix}${params.prompt ?? ""}${jsonReminder}`;
256
+ args.push("--prompt", fullPrompt);
257
+ const accountEnv = {};
258
+ if (this.opts.apiKey)
259
+ accountEnv.GEMINI_API_KEY = this.opts.apiKey;
260
+ return {
261
+ command: this.opts.binary ?? "agy",
262
+ args,
263
+ outputFormat,
264
+ env: Object.keys(accountEnv).length > 0 ? accountEnv : undefined,
265
+ };
266
+ }
267
+ }
@@ -0,0 +1,38 @@
1
+ import type { BaseCliAgentOptions } from "./BaseCliAgent/BaseCliAgentOptions";
2
+
3
+ export type AntigravityAgentOptions = BaseCliAgentOptions & {
4
+ debug?: boolean;
5
+ model?: string;
6
+ sandbox?: boolean;
7
+ yolo?: boolean;
8
+ dangerouslySkipPermissions?: boolean;
9
+ allowedMcpServerNames?: string[];
10
+ allowedTools?: string[];
11
+ extensions?: string[];
12
+ listExtensions?: boolean;
13
+ resume?: string;
14
+ listSessions?: boolean;
15
+ deleteSession?: string;
16
+ includeDirectories?: string[];
17
+ screenReader?: boolean;
18
+ outputFormat?: "text" | "json" | "stream-json";
19
+ /**
20
+ * Antigravity CLI binary to execute. The official CLI currently installs
21
+ * `agy`; this exists for test harnesses and future binary renames.
22
+ */
23
+ binary?: string;
24
+ /**
25
+ * Path to an isolated Google CLI config root. Passed as `--gemini_dir` so
26
+ * Antigravity reads/writes `<configDir>/antigravity-cli/...` instead of the
27
+ * user's default `~/.gemini/antigravity-cli/...`.
28
+ */
29
+ configDir?: string;
30
+ /**
31
+ * Explicit alias for `configDir` when matching the Antigravity CLI flag name.
32
+ */
33
+ geminiDir?: string;
34
+ /**
35
+ * Google API key for API-billed invocations when supported by the CLI.
36
+ */
37
+ apiKey?: string;
38
+ };
@@ -458,6 +458,7 @@ export function extractUsageFromOutput(raw) {
458
458
  const lines = stripOscSequences(raw).split(/\r?\n/).filter(Boolean);
459
459
  const usage = {};
460
460
  let found = false;
461
+ let countedIncremental = false;
461
462
  for (const line of lines) {
462
463
  let parsed;
463
464
  try {
@@ -480,6 +481,7 @@ export function extractUsageFromOutput(raw) {
480
481
  (usage.cacheWriteTokens ?? 0) + u.cache_creation_input_tokens;
481
482
  }
482
483
  found = true;
484
+ countedIncremental = true;
483
485
  continue;
484
486
  }
485
487
  if (parsed.type === "message_delta" && parsed.usage) {
@@ -488,8 +490,19 @@ export function extractUsageFromOutput(raw) {
488
490
  (usage.outputTokens ?? 0) + parsed.usage.output_tokens;
489
491
  }
490
492
  found = true;
493
+ countedIncremental = true;
491
494
  continue;
492
495
  }
496
+ if (parsed.type === "result") {
497
+ // Claude Code stream-json emits a terminal "result" event whose
498
+ // top-level usage summarizes tokens already accumulated from the
499
+ // per-message message_start/message_delta events. If we counted
500
+ // those incrementally, skip this event to avoid double-counting.
501
+ // Otherwise fall through so the usage is still captured.
502
+ if (countedIncremental) {
503
+ continue;
504
+ }
505
+ }
493
506
  if (parsed.type === "turn.completed" && parsed.usage) {
494
507
  const u = parsed.usage;
495
508
  if (u.input_tokens) {
@@ -13,7 +13,7 @@ import { truncateToBytes } from "./truncateToBytes.js";
13
13
 
14
14
  /** @typedef {import("./PiExtensionUiRequest.ts").PiExtensionUiRequest} PiExtensionUiRequest */
15
15
  /**
16
- * @typedef {{ cwd: string; env: Record<string, string>; prompt: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; onJsonEvent?: (event: Record<string, unknown>) => Promise<void> | void; onExtensionUiRequest?: (request: PiExtensionUiRequest) => Promise<PiExtensionUiResponse | null> | PiExtensionUiResponse | null; }} RunRpcCommandOptions
16
+ * @typedef {{ cwd: string; env: Record<string, string>; prompt: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; onJsonEvent?: (event: Record<string, unknown>) => Promise<void> | void; onExtensionUiRequest?: (request: PiExtensionUiRequest) => Promise<PiExtensionUiResponse | null> | PiExtensionUiResponse | null; spawnFn?: typeof spawn; }} RunRpcCommandOptions
17
17
  */
18
18
 
19
19
  /**
@@ -61,7 +61,7 @@ function createInactivityTimer(timeoutMs, onTimeout) {
61
61
  * @returns {Effect.Effect<{ text: string; output: unknown; stderr: string; exitCode: number | null; usage?: any; }, SmithersError>}
62
62
  */
63
63
  export function runRpcCommandEffect(command, args, options) {
64
- const { cwd, env, prompt, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, onStdout, onStderr, onJsonEvent, onExtensionUiRequest, } = options;
64
+ const { cwd, env, prompt, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, onStdout, onStderr, onJsonEvent, onExtensionUiRequest, spawnFn = spawn, } = options;
65
65
  const span = `agent:${command}:rpc`;
66
66
  const logAnnotations = {
67
67
  agentCommand: command,
@@ -82,7 +82,7 @@ export function runRpcCommandEffect(command, args, options) {
82
82
  let extractedUsage = undefined;
83
83
  let stderrTruncated = false;
84
84
  logDebug("starting agent RPC command", logAnnotations, span);
85
- const child = spawn(command, args, {
85
+ const child = spawnFn(command, args, {
86
86
  cwd,
87
87
  env,
88
88
  detached: true,
@@ -108,6 +108,8 @@ export function runRpcCommandEffect(command, args, options) {
108
108
  if (settled)
109
109
  return;
110
110
  settled = true;
111
+ inactivity.clear();
112
+ totalTimeout.clear();
111
113
  if (signal) {
112
114
  signal.removeEventListener("abort", onAbort);
113
115
  }
@@ -131,6 +133,8 @@ export function runRpcCommandEffect(command, args, options) {
131
133
  if (settled)
132
134
  return;
133
135
  settled = true;
136
+ inactivity.clear();
137
+ totalTimeout.clear();
134
138
  if (signal) {
135
139
  signal.removeEventListener("abort", onAbort);
136
140
  }
@@ -9,5 +9,22 @@ export function truncateToBytes(text, maxBytes) {
9
9
  const buf = Buffer.from(text, "utf8");
10
10
  if (buf.length <= maxBytes)
11
11
  return text;
12
- return buf.subarray(0, maxBytes).toString("utf8");
12
+ let end = maxBytes;
13
+ // Back off any UTF-8 continuation bytes (0b10xxxxxx) so the slice ends on a
14
+ // codepoint boundary, then drop the lead byte if its sequence is incomplete.
15
+ while (end > 0 && (buf[end] & 0xc0) === 0x80)
16
+ end--;
17
+ if (end > 0) {
18
+ const lead = buf[end - 1];
19
+ let seqLen = 1;
20
+ if ((lead & 0xe0) === 0xc0)
21
+ seqLen = 2;
22
+ else if ((lead & 0xf0) === 0xe0)
23
+ seqLen = 3;
24
+ else if ((lead & 0xf8) === 0xf0)
25
+ seqLen = 4;
26
+ if ((end - 1) + seqLen > maxBytes)
27
+ end--;
28
+ }
29
+ return buf.subarray(0, end).toString("utf8");
13
30
  }
package/src/CodexAgent.js CHANGED
@@ -510,7 +510,7 @@ export class CodexAgent extends BaseCliAgent {
510
510
  const resumeSession = typeof params.options?.resumeSession === "string"
511
511
  ? params.options.resumeSession
512
512
  : undefined;
513
- const args = resumeSession ? ["exec", "resume", resumeSession] : ["exec"];
513
+ const args = resumeSession ? ["exec", "resume"] : ["exec"];
514
514
  const yoloEnabled = this.opts.yolo ?? this.yolo;
515
515
  const configOverrides = normalizeCodexConfig(this.opts.config);
516
516
  for (const entry of configOverrides) {
@@ -520,21 +520,26 @@ export class CodexAgent extends BaseCliAgent {
520
520
  pushList(args, "--disable", this.opts.disable);
521
521
  pushList(args, "--image", this.opts.image);
522
522
  pushFlag(args, "--model", this.opts.model ?? this.model);
523
- if (this.opts.oss)
523
+ if (!resumeSession && this.opts.oss)
524
524
  args.push("--oss");
525
- pushFlag(args, "--local-provider", this.opts.localProvider);
526
- pushFlag(args, "--sandbox", this.opts.sandbox);
527
- pushFlag(args, "--profile", this.opts.profile);
528
- if (this.opts.fullAuto) {
525
+ if (!resumeSession)
526
+ pushFlag(args, "--local-provider", this.opts.localProvider);
527
+ if (!resumeSession)
528
+ pushFlag(args, "--sandbox", this.opts.sandbox);
529
+ if (!resumeSession)
530
+ pushFlag(args, "--profile", this.opts.profile);
531
+ if (!resumeSession && this.opts.fullAuto) {
529
532
  args.push("--full-auto");
530
533
  }
531
534
  else if (yoloEnabled || this.opts.dangerouslyBypassApprovalsAndSandbox) {
532
535
  args.push("--dangerously-bypass-approvals-and-sandbox");
533
536
  }
534
- pushFlag(args, "--cd", this.opts.cd);
537
+ if (!resumeSession)
538
+ pushFlag(args, "--cd", this.opts.cd);
535
539
  if (this.opts.skipGitRepoCheck)
536
540
  args.push("--skip-git-repo-check");
537
- pushList(args, "--add-dir", this.opts.addDir);
541
+ if (!resumeSession)
542
+ pushList(args, "--add-dir", this.opts.addDir);
538
543
  if (!resumeSession) {
539
544
  pushFlag(args, "--output-schema", this.opts.outputSchema);
540
545
  }
@@ -562,6 +567,8 @@ export class CodexAgent extends BaseCliAgent {
562
567
  pushFlag(args, "--output-last-message", outputFile);
563
568
  if (this.extraArgs?.length)
564
569
  args.push(...this.extraArgs);
570
+ if (resumeSession)
571
+ args.push(resumeSession);
565
572
  const systemPrefix = params.systemPrompt
566
573
  ? `${params.systemPrompt}\n\n`
567
574
  : "";
@@ -38,6 +38,10 @@ export function createGeminiCapabilityRegistry(opts = {}) {
38
38
  builtIns: resolveGeminiBuiltIns(opts),
39
39
  };
40
40
  }
41
+ /**
42
+ * @deprecated Use AntigravityAgent for new Google CLI integrations. GeminiAgent
43
+ * remains for legacy and enterprise Gemini CLI setups.
44
+ */
41
45
  export class GeminiAgent extends BaseCliAgent {
42
46
  opts;
43
47
  capabilities;
@@ -1,5 +1,10 @@
1
1
  import type { BaseCliAgentOptions } from "./BaseCliAgent/BaseCliAgentOptions";
2
2
 
3
+ /**
4
+ * @deprecated Use AntigravityAgentOptions with the Antigravity CLI (`agy`) for
5
+ * new Google CLI integrations. GeminiAgentOptions remains for legacy and
6
+ * enterprise Gemini CLI setups.
7
+ */
3
8
  export type GeminiAgentOptions = BaseCliAgentOptions & {
4
9
  debug?: boolean;
5
10
  model?: string;
@@ -0,0 +1,41 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ import { OpenAIAgent } from "./OpenAIAgent.js";
3
+
4
+ /**
5
+ * @template [CALL_OPTIONS=never], [TOOLS=import("ai").ToolSet]
6
+ * @typedef {import("./HermesAgentOptions.ts").HermesAgentOptions<CALL_OPTIONS, TOOLS>} HermesAgentOptions
7
+ */
8
+
9
+ /**
10
+ * Hermes (Nous Research) agent, reached over its OpenAI-compatible HTTP API.
11
+ *
12
+ * A thin wrapper over {@link OpenAIAgent}: it points the OpenAI-compatible
13
+ * provider at the Hermes server (`baseURL` / `HERMES_BASE_URL`) and disables AI
14
+ * SDK native structured output by default, since a local Hermes server may not
15
+ * honor JSON-schema response formats. Everything else — tool loops, streaming,
16
+ * prompt-based structured output — comes from the shared OpenAI path.
17
+ *
18
+ * @template [CALL_OPTIONS=never], [TOOLS=import("ai").ToolSet]
19
+ */
20
+ export class HermesAgent extends OpenAIAgent {
21
+ /**
22
+ * @param {HermesAgentOptions<CALL_OPTIONS, TOOLS>} [opts]
23
+ */
24
+ constructor(opts = {}) {
25
+ const {
26
+ model = "hermes",
27
+ baseURL = process.env.HERMES_BASE_URL,
28
+ apiKey = process.env.HERMES_API_KEY ?? "hermes",
29
+ nativeStructuredOutput = false,
30
+ ...rest
31
+ } = opts;
32
+ if (baseURL === undefined) {
33
+ throw new SmithersError(
34
+ "AGENT_CONFIG_INVALID",
35
+ "HermesAgent requires a baseURL (or the HERMES_BASE_URL env var) pointing at the Hermes OpenAI-compatible API, e.g. http://127.0.0.1:5123/v1.",
36
+ {},
37
+ );
38
+ }
39
+ super({ ...rest, model, baseURL, apiKey, nativeStructuredOutput });
40
+ }
41
+ }
@@ -0,0 +1,41 @@
1
+ import type { openai } from "@ai-sdk/openai";
2
+ import type { ToolSet } from "ai";
3
+ import type { SdkAgentOptions } from "./SdkAgentOptions";
4
+
5
+ /**
6
+ * Options for {@link HermesAgent}.
7
+ *
8
+ * Hermes (Nous Research) exposes an OpenAI-compatible HTTP API
9
+ * (`/v1/chat/completions`), so a Hermes agent is reached the same way as any
10
+ * OpenAI-compatible endpoint: point `baseURL` at the Hermes server. These mirror
11
+ * the string-model form of `OpenAIAgentOptions`.
12
+ */
13
+ export type HermesAgentOptions<
14
+ CALL_OPTIONS = never,
15
+ TOOLS extends ToolSet = {},
16
+ > = Omit<
17
+ SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof openai>>,
18
+ "model"
19
+ > & {
20
+ /**
21
+ * Model name exposed by your Hermes server. Defaults to `"hermes"`; override
22
+ * with whatever model id the server advertises.
23
+ */
24
+ model?: string;
25
+ /**
26
+ * Base URL of the Hermes OpenAI-compatible API, e.g. `http://127.0.0.1:5123/v1`.
27
+ * Falls back to the `HERMES_BASE_URL` environment variable.
28
+ */
29
+ baseURL?: string;
30
+ /**
31
+ * API key sent to the Hermes server. Falls back to `HERMES_API_KEY`, then
32
+ * `"hermes"` (local servers commonly ignore the value).
33
+ */
34
+ apiKey?: string;
35
+ /**
36
+ * Enable AI SDK native structured output. Off by default because a local
37
+ * Hermes server may not honor JSON-schema response formats — leaving it off
38
+ * makes Smithers fall back to prompt-based JSON extraction.
39
+ */
40
+ nativeStructuredOutput?: boolean;
41
+ };
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AgentLike,
3
3
  AmpAgent,
4
+ AntigravityAgent,
4
5
  AnthropicAgent,
5
6
  ClaudeCodeAgent,
6
7
  CodexAgent,
@@ -20,6 +21,7 @@ type _CustomNativeStructuredAgent = AssertAssignable<{
20
21
 
21
22
  type _ConcreteAgentsAreAgentLike = [
22
23
  AssertAssignable<AmpAgent>,
24
+ AssertAssignable<AntigravityAgent>,
23
25
  AssertAssignable<AnthropicAgent>,
24
26
  AssertAssignable<ClaudeCodeAgent>,
25
27
  AssertAssignable<CodexAgent>,
@@ -2,7 +2,7 @@ import type { AgentToolDescriptor } from "./AgentToolDescriptor";
2
2
 
3
3
  export type AgentCapabilityRegistry = {
4
4
  version: 1;
5
- engine: "claude-code" | "codex" | "gemini" | "kimi" | "pi" | "amp" | "forge" | "opencode";
5
+ engine: "claude-code" | "codex" | "antigravity" | "gemini" | "kimi" | "pi" | "amp" | "forge" | "opencode";
6
6
  runtimeTools: Record<string, AgentToolDescriptor>;
7
7
  mcp: {
8
8
  bootstrap: "inline-config" | "project-config" | "allow-list" | "unsupported";
@@ -1,5 +1,6 @@
1
1
  export type CliAgentCapabilityAdapterId =
2
2
  | "claude"
3
+ | "antigravity"
3
4
  | "codex"
4
5
  | "gemini"
5
6
  | "kimi"
@@ -1,4 +1,5 @@
1
1
  import { hashCapabilityRegistry, normalizeCapabilityRegistry, } from "../capability-registry/index.js";
2
+ import { createAntigravityCapabilityRegistry } from "../AntigravityAgent.js";
2
3
  import { createClaudeCodeCapabilityRegistry } from "../ClaudeCodeAgent.js";
3
4
  import { createCodexCapabilityRegistry } from "../CodexAgent.js";
4
5
  import { createGeminiCapabilityRegistry } from "../GeminiAgent.js";
@@ -18,6 +19,11 @@ const CLI_AGENT_CAPABILITY_ADAPTERS = [
18
19
  binary: "codex",
19
20
  buildRegistry: () => createCodexCapabilityRegistry(),
20
21
  },
22
+ {
23
+ id: "antigravity",
24
+ binary: "agy",
25
+ buildRegistry: () => createAntigravityCapabilityRegistry(),
26
+ },
21
27
  {
22
28
  id: "gemini",
23
29
  binary: "gemini",
@@ -321,7 +321,7 @@ const codexStrategy = {
321
321
  ],
322
322
  };
323
323
  // ---------------------------------------------------------------------------
324
- // Gemini strategy
324
+ // Google CLI strategies
325
325
  // ---------------------------------------------------------------------------
326
326
  // Validate Google auth via GET /v1beta/models (free, no tokens)
327
327
  const googleAuthCheck = {
@@ -438,6 +438,37 @@ const geminiStrategy = {
438
438
  googleRateLimitCheck,
439
439
  ],
440
440
  };
441
+ const antigravityAuthSkip = {
442
+ id: "api_key_valid",
443
+ run: async () => {
444
+ return {
445
+ id: "api_key_valid",
446
+ status: "skip",
447
+ message: "Antigravity CLI uses Google Sign-In/keyring auth; run `agy` to authenticate.",
448
+ durationMs: 0,
449
+ };
450
+ },
451
+ };
452
+ const antigravityRateLimitSkip = {
453
+ id: "rate_limit_status",
454
+ run: async () => {
455
+ return {
456
+ id: "rate_limit_status",
457
+ status: "skip",
458
+ message: "Antigravity CLI rate limits are checked by the CLI at runtime.",
459
+ durationMs: 0,
460
+ };
461
+ },
462
+ };
463
+ const antigravityStrategy = {
464
+ agentId: "antigravity",
465
+ command: "agy",
466
+ checks: [
467
+ checkCliInstalled("agy", "Antigravity CLI"),
468
+ antigravityAuthSkip,
469
+ antigravityRateLimitSkip,
470
+ ],
471
+ };
441
472
  // ---------------------------------------------------------------------------
442
473
  // Pi strategy
443
474
  // ---------------------------------------------------------------------------
@@ -490,6 +521,8 @@ const ampStrategy = {
490
521
  const strategies = {
491
522
  claude: claudeStrategy,
492
523
  codex: codexStrategy,
524
+ antigravity: antigravityStrategy,
525
+ agy: antigravityStrategy,
493
526
  gemini: geminiStrategy,
494
527
  pi: piStrategy,
495
528
  amp: ampStrategy,
@@ -19,10 +19,15 @@ const PER_CHECK_TIMEOUT_MS = 5_000;
19
19
  */
20
20
  async function runCheck(check, ctx) {
21
21
  const start = performance.now();
22
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
23
+ let timeoutHandle;
22
24
  try {
23
25
  return await Promise.race([
24
26
  check.run(ctx),
25
- new Promise((_, reject) => setTimeout(() => reject(new SmithersError("AGENT_DIAGNOSTIC_TIMEOUT", "diagnostic check timed out", { timeoutMs: PER_CHECK_TIMEOUT_MS })), PER_CHECK_TIMEOUT_MS)),
27
+ new Promise((_, reject) => {
28
+ timeoutHandle = setTimeout(() => reject(new SmithersError("AGENT_DIAGNOSTIC_TIMEOUT", "diagnostic check timed out", { timeoutMs: PER_CHECK_TIMEOUT_MS })), PER_CHECK_TIMEOUT_MS);
29
+ timeoutHandle.unref?.();
30
+ }),
26
31
  ]);
27
32
  }
28
33
  catch (err) {
@@ -33,6 +38,9 @@ async function runCheck(check, ctx) {
33
38
  durationMs: performance.now() - start,
34
39
  };
35
40
  }
41
+ finally {
42
+ clearTimeout(timeoutHandle);
43
+ }
36
44
  }
37
45
  /**
38
46
  * @param {AgentDiagnosticStrategy} strategy
package/src/index.d.ts CHANGED
@@ -138,7 +138,7 @@ type AgentToolDescriptor$1 = {
138
138
 
139
139
  type AgentCapabilityRegistry$3 = {
140
140
  version: 1;
141
- engine: "claude-code" | "codex" | "gemini" | "kimi" | "pi" | "amp" | "forge" | "opencode";
141
+ engine: "claude-code" | "codex" | "antigravity" | "gemini" | "kimi" | "pi" | "amp" | "forge" | "opencode";
142
142
  runtimeTools: Record<string, AgentToolDescriptor$1>;
143
143
  mcp: {
144
144
  bootstrap: "inline-config" | "project-config" | "allow-list" | "unsupported";
@@ -364,6 +364,19 @@ declare class OpenAIAgent extends ToolLoopAgent<never, any, never> {
364
364
  type GenerateTextResult$1 = ai.GenerateTextResult<any, any>;
365
365
  type OpenAIAgentOptions$1<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = OpenAIAgentOptions$2<CALL_OPTIONS, TOOLS>;
366
366
 
367
+ type HermesAgentOptions$2<CALL_OPTIONS = never, TOOLS extends ToolSet = {}> = Omit<SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof openai>>, "model"> & {
368
+ model?: string;
369
+ baseURL?: string;
370
+ apiKey?: string;
371
+ nativeStructuredOutput?: boolean;
372
+ };
373
+ /**
374
+ * Hermes (Nous Research) agent, reached over its OpenAI-compatible HTTP API.
375
+ */
376
+ declare class HermesAgent extends OpenAIAgent {
377
+ constructor(opts?: HermesAgentOptions$2);
378
+ }
379
+
367
380
  /**
368
381
  * Configuration options for the AmpAgent.
369
382
  */
@@ -425,6 +438,57 @@ declare class AmpAgent extends BaseCliAgent {
425
438
  type AmpAgentOptions = AmpAgentOptions$1;
426
439
  type CliOutputInterpreter$6 = CliOutputInterpreter$8;
427
440
 
441
+ type AntigravityAgentOptions$1 = BaseCliAgentOptions$1 & {
442
+ debug?: boolean;
443
+ model?: string;
444
+ sandbox?: boolean;
445
+ yolo?: boolean;
446
+ dangerouslySkipPermissions?: boolean;
447
+ allowedMcpServerNames?: string[];
448
+ allowedTools?: string[];
449
+ extensions?: string[];
450
+ listExtensions?: boolean;
451
+ resume?: string;
452
+ listSessions?: boolean;
453
+ deleteSession?: string;
454
+ includeDirectories?: string[];
455
+ screenReader?: boolean;
456
+ outputFormat?: "text" | "json" | "stream-json";
457
+ binary?: string;
458
+ configDir?: string;
459
+ geminiDir?: string;
460
+ apiKey?: string;
461
+ };
462
+ declare function createAntigravityCapabilityRegistry(opts?: AntigravityAgentOptions): AgentCapabilityRegistry$3;
463
+ declare class AntigravityAgent extends BaseCliAgent {
464
+ /**
465
+ * @param {AntigravityAgentOptions} [opts]
466
+ */
467
+ constructor(opts?: AntigravityAgentOptions);
468
+ opts: AntigravityAgentOptions$1;
469
+ capabilities: AgentCapabilityRegistry$3;
470
+ cliEngine: string;
471
+ /**
472
+ * @returns {CliOutputInterpreter}
473
+ */
474
+ createOutputInterpreter(): CliOutputInterpreter$6;
475
+ /**
476
+ * @param {{ prompt: string; systemPrompt?: string; cwd: string; options: any; }} params
477
+ */
478
+ buildCommand(params: {
479
+ prompt: string;
480
+ systemPrompt?: string;
481
+ cwd: string;
482
+ options: any;
483
+ }): Promise<{
484
+ command: string;
485
+ args: string[];
486
+ outputFormat: "text" | "json" | "stream-json";
487
+ env: Record<string, string> | undefined;
488
+ }>;
489
+ }
490
+ type AntigravityAgentOptions = AntigravityAgentOptions$1;
491
+
428
492
  type ClaudeCodeAgentOptions$1 = BaseCliAgentOptions$1 & {
429
493
  addDir?: string[];
430
494
  agent?: string;
@@ -887,6 +951,7 @@ type AgentToolDescriptor = AgentToolDescriptor$1;
887
951
  type AnthropicAgentOptions<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = AnthropicAgentOptions$2<CALL_OPTIONS, TOOLS>;
888
952
  type OpenCodeAgentOptions = OpenCodeAgentOptions$1;
889
953
  type OpenAIAgentOptions<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = OpenAIAgentOptions$2<CALL_OPTIONS, TOOLS>;
954
+ type HermesAgentOptions<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = HermesAgentOptions$2<CALL_OPTIONS, TOOLS>;
890
955
  type PiAgentOptions = PiAgentOptions$2;
891
956
  type PiExtensionUiRequest = PiExtensionUiRequest$1;
892
957
  type PiExtensionUiResponse = PiExtensionUiResponse$1;
@@ -896,4 +961,4 @@ type SmithersAgentToolCategory = SmithersAgentToolCategory$1;
896
961
  type SmithersListedTool = SmithersListedTool$2;
897
962
  type SmithersToolSurface = SmithersToolSurface$2;
898
963
 
899
- export { type AgentCapabilityRegistry, type AgentGenerateOptions, type AgentLike, type AgentToolDescriptor, AmpAgent, AnthropicAgent, type AnthropicAgentOptions, BaseCliAgent, ClaudeCodeAgent, CodexAgent, ForgeAgent, GeminiAgent, KimiAgent, OpenAIAgent, type OpenAIAgentOptions, OpenCodeAgent, type OpenCodeAgentOptions, PiAgent, type PiAgentOptions, type PiExtensionUiRequest, type PiExtensionUiResponse, type SmithersAgentContract, type SmithersAgentContractTool, type SmithersAgentToolCategory, type SmithersListedTool, type SmithersToolSurface, createSmithersAgentContract, hashCapabilityRegistry, renderSmithersAgentPromptGuidance, sanitizeForOpenAI, zodToOpenAISchema };
964
+ export { type AgentCapabilityRegistry, type AgentGenerateOptions, type AgentLike, type AgentToolDescriptor, AmpAgent, AnthropicAgent, type AnthropicAgentOptions, AntigravityAgent, BaseCliAgent, ClaudeCodeAgent, CodexAgent, ForgeAgent, GeminiAgent, HermesAgent, type HermesAgentOptions, KimiAgent, OpenAIAgent, type OpenAIAgentOptions, OpenCodeAgent, type OpenCodeAgentOptions, PiAgent, type PiAgentOptions, type PiExtensionUiRequest, type PiExtensionUiResponse, type SmithersAgentContract, type SmithersAgentContractTool, type SmithersAgentToolCategory, type SmithersListedTool, type SmithersToolSurface, createAntigravityCapabilityRegistry, createSmithersAgentContract, hashCapabilityRegistry, renderSmithersAgentPromptGuidance, sanitizeForOpenAI, zodToOpenAISchema };
package/src/index.js CHANGED
@@ -13,6 +13,11 @@
13
13
  * @template [TOOLS=import("ai").ToolSet]
14
14
  * @typedef {import("./OpenAIAgentOptions.ts").OpenAIAgentOptions<CALL_OPTIONS, TOOLS>} OpenAIAgentOptions
15
15
  */
16
+ /**
17
+ * @template [CALL_OPTIONS=never]
18
+ * @template [TOOLS=import("ai").ToolSet]
19
+ * @typedef {import("./HermesAgentOptions.ts").HermesAgentOptions<CALL_OPTIONS, TOOLS>} HermesAgentOptions
20
+ */
16
21
  /** @typedef {import("./PiAgentOptions.ts").PiAgentOptions} PiAgentOptions */
17
22
  /** @typedef {import("./BaseCliAgent/PiExtensionUiRequest.ts").PiExtensionUiRequest} PiExtensionUiRequest */
18
23
  /** @typedef {import("./BaseCliAgent/PiExtensionUiResponse.ts").PiExtensionUiResponse} PiExtensionUiResponse */
@@ -28,7 +33,9 @@ export { BaseCliAgent } from "./BaseCliAgent/index.js";
28
33
  export { hashCapabilityRegistry } from "./capability-registry/index.js";
29
34
  export { AnthropicAgent } from "./AnthropicAgent.js";
30
35
  export { OpenAIAgent } from "./OpenAIAgent.js";
36
+ export { HermesAgent } from "./HermesAgent.js";
31
37
  export { AmpAgent } from "./AmpAgent.js";
38
+ export { AntigravityAgent, createAntigravityCapabilityRegistry } from "./AntigravityAgent.js";
32
39
  export { ClaudeCodeAgent } from "./ClaudeCodeAgent.js";
33
40
  export { CodexAgent } from "./CodexAgent.js";
34
41
  export { GeminiAgent } from "./GeminiAgent.js";