@smithers-orchestrator/agents 0.21.0 → 0.23.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.
Files changed (34) hide show
  1. package/package.json +6 -5
  2. package/src/AmpAgent.js +26 -19
  3. package/src/AntigravityAgent.js +53 -18
  4. package/src/AntigravityAgentOptions.ts +45 -4
  5. package/src/BaseCliAgent/AgentGenerateOptions.ts +12 -0
  6. package/src/BaseCliAgent/BaseCliAgent.js +19 -1
  7. package/src/BaseCliAgent/runRpcCommandEffect.js +7 -3
  8. package/src/BaseCliAgent/taskContextEnv.js +31 -0
  9. package/src/BaseCliAgent/truncateToBytes.js +18 -1
  10. package/src/ClaudeCodeAgent.js +19 -1
  11. package/src/CodexAgent.js +15 -8
  12. package/src/ForgeAgent.js +26 -19
  13. package/src/HermesAgent.js +41 -0
  14. package/src/HermesAgentOptions.ts +41 -0
  15. package/src/VibeAgent.js +214 -0
  16. package/src/VibeAgentOptions.ts +11 -0
  17. package/src/agent-contract/createSmithersAgentContract.js +1 -0
  18. package/src/agent-contract/renderSmithersAgentPromptGuidance.js +4 -0
  19. package/src/capability-registry/AgentCapabilityRegistry.ts +1 -1
  20. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +4 -1
  21. package/src/cli-capabilities/CliAgentCapabilityReportEntry.ts +2 -0
  22. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +48 -1
  23. package/src/cli-capabilities/getCliAgentCapabilityReport.js +24 -0
  24. package/src/cli-capabilities/index.js +5 -0
  25. package/src/cli-surface/CliAgentSurfaceTypes.ts +34 -0
  26. package/src/cli-surface/cliAgentSurfaceManifest.js +490 -0
  27. package/src/cli-surface/index.js +5 -0
  28. package/src/diagnostics/runDiagnostics.js +9 -1
  29. package/src/index.d.ts +715 -380
  30. package/src/index.js +27 -0
  31. package/src/mcp/McpServerConfig.ts +19 -0
  32. package/src/mcp/McpToolset.ts +17 -0
  33. package/src/mcp/createMcpToolset.js +94 -0
  34. package/src/sanitizeForOpenAI.js +20 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "AI SDK and CLI agent adapters for Smithers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -47,12 +47,13 @@
47
47
  "dependencies": {
48
48
  "@ai-sdk/anthropic": "^3.0.71",
49
49
  "@ai-sdk/openai": "^3.0.53",
50
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
51
  "ai": "^6.0.168",
51
52
  "effect": "^3.21.1",
52
53
  "zod": "^4.3.6",
53
- "@smithers-orchestrator/errors": "0.21.0",
54
- "@smithers-orchestrator/observability": "0.21.0",
55
- "@smithers-orchestrator/driver": "0.21.0"
54
+ "@smithers-orchestrator/errors": "0.23.0",
55
+ "@smithers-orchestrator/driver": "0.23.0",
56
+ "@smithers-orchestrator/observability": "0.23.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/bun": "latest",
@@ -60,7 +61,7 @@
60
61
  "typescript": "~5.9.3"
61
62
  },
62
63
  "scripts": {
63
- "test": "bun test tests",
64
+ "test": "bun test --timeout=60000 --max-concurrency=1 tests",
64
65
  "typecheck": "tsc -p tsconfig.json --noEmit",
65
66
  "build": "tsup --dts-only"
66
67
  }
package/src/AmpAgent.js CHANGED
@@ -6,6 +6,31 @@ import { BaseCliAgent, pushFlag, isRecord, asString, toolKindFromName, createSyn
6
6
  /** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
7
7
  /** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
8
8
 
9
+ /**
10
+ * @returns {AgentCapabilityRegistry}
11
+ */
12
+ export function createAmpCapabilityRegistry() {
13
+ return {
14
+ version: 1,
15
+ engine: "amp",
16
+ runtimeTools: {},
17
+ mcp: {
18
+ bootstrap: "project-config",
19
+ supportsProjectScope: true,
20
+ supportsUserScope: false,
21
+ },
22
+ skills: {
23
+ supportsSkills: false,
24
+ smithersSkillIds: [],
25
+ },
26
+ humanInteraction: {
27
+ supportsUiRequests: false,
28
+ methods: [],
29
+ },
30
+ builtIns: ["default"],
31
+ };
32
+ }
33
+
9
34
  /**
10
35
  * Agent implementation that wraps the 'amp' CLI executable.
11
36
  * It translates generation requests into CLI arguments and executes the process.
@@ -23,25 +48,7 @@ export class AmpAgent extends BaseCliAgent {
23
48
  constructor(opts = {}) {
24
49
  super(opts);
25
50
  this.opts = opts;
26
- this.capabilities = {
27
- version: 1,
28
- engine: "amp",
29
- runtimeTools: {},
30
- mcp: {
31
- bootstrap: "project-config",
32
- supportsProjectScope: true,
33
- supportsUserScope: false,
34
- },
35
- skills: {
36
- supportsSkills: false,
37
- smithersSkillIds: [],
38
- },
39
- humanInteraction: {
40
- supportsUiRequests: false,
41
- methods: [],
42
- },
43
- builtIns: ["default"],
44
- };
51
+ this.capabilities = createAmpCapabilityRegistry();
45
52
  }
46
53
  /**
47
54
  * @returns {CliOutputInterpreter}
@@ -1,9 +1,51 @@
1
1
  import { BaseCliAgent, pushFlag, pushList, isRecord, asString, truncate, toolKindFromName, createSyntheticIdGenerator, } from "./BaseCliAgent/index.js";
2
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
3
  import { normalizeCapabilityStringList, } from "./capability-registry/index.js";
4
+ import { getCliAgentSurfaceManifestEntry } from "./cli-surface/index.js";
3
5
  /** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
4
6
  /** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
5
7
  /** @typedef {import("./AntigravityAgentOptions.ts").AntigravityAgentOptions} AntigravityAgentOptions */
6
8
 
9
+ const ANTIGRAVITY_SURFACE = getCliAgentSurfaceManifestEntry("antigravity");
10
+
11
+ /**
12
+ * @param {string} option
13
+ * @param {string} flag
14
+ * @returns {SmithersError}
15
+ */
16
+ function unsupportedAntigravityOption(option, flag) {
17
+ const rule = ANTIGRAVITY_SURFACE?.unsupportedFlags.find((entry) => entry.flag === flag);
18
+ const replacement = rule?.replacement ? ` Use ${rule.replacement} instead.` : "";
19
+ const reason = rule?.reason ? ` ${rule.reason}` : "";
20
+ return new SmithersError("AGENT_CONFIG_INVALID", `AntigravityAgent option "${option}" maps to unsupported agy flag ${flag}.${reason}${replacement}`, {
21
+ agentEngine: "antigravity",
22
+ option,
23
+ flag,
24
+ replacement: rule?.replacement,
25
+ failureRetryable: false,
26
+ });
27
+ }
28
+
29
+ /**
30
+ * @param {AntigravityAgentOptions} opts
31
+ */
32
+ function assertSupportedAntigravityOptions(opts) {
33
+ if (opts.debug)
34
+ throw unsupportedAntigravityOption("debug", "--debug");
35
+ if (opts.screenReader)
36
+ throw unsupportedAntigravityOption("screenReader", "--screen-reader");
37
+ if (opts.outputFormat !== undefined)
38
+ throw unsupportedAntigravityOption("outputFormat", "--output-format");
39
+ if (opts.listSessions)
40
+ throw unsupportedAntigravityOption("listSessions", "--list-sessions");
41
+ if (opts.deleteSession !== undefined)
42
+ throw unsupportedAntigravityOption("deleteSession", "--delete-session");
43
+ if (opts.extensions?.length)
44
+ throw unsupportedAntigravityOption("extensions", "--extensions");
45
+ if (opts.listExtensions)
46
+ throw unsupportedAntigravityOption("listExtensions", "--list-extensions");
47
+ }
48
+
7
49
  /**
8
50
  * @param {AntigravityAgentOptions} opts
9
51
  */
@@ -209,15 +251,13 @@ export class AntigravityAgent extends BaseCliAgent {
209
251
  * @param {{ prompt: string; systemPrompt?: string; cwd: string; options: any; }} params
210
252
  */
211
253
  async buildCommand(params) {
254
+ assertSupportedAntigravityOptions(this.opts);
212
255
  const args = [];
213
256
  const yoloEnabled = this.opts.dangerouslySkipPermissions ?? this.opts.yolo ?? this.yolo;
214
- const outputFormat = this.opts.outputFormat ??
215
- (params.options?.onEvent ? "stream-json" : "json");
216
257
  const resumeSession = typeof params.options?.resumeSession === "string"
217
258
  ? params.options.resumeSession
218
- : this.opts.resume;
219
- if (this.opts.debug)
220
- args.push("--debug");
259
+ : this.opts.conversation ?? this.opts.resume;
260
+ args.push("--cwd", params.cwd);
221
261
  pushFlag(args, "--model", this.opts.model ?? this.model);
222
262
  if (this.opts.sandbox)
223
263
  args.push("--sandbox");
@@ -232,18 +272,11 @@ export class AntigravityAgent extends BaseCliAgent {
232
272
  pushList(args, "--allowed-tools", this.opts.allowedTools);
233
273
  }
234
274
  }
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");
275
+ if (this.opts.continue)
276
+ args.push("--continue");
277
+ pushFlag(args, "--conversation", resumeSession);
278
+ pushList(args, "--add-dir", this.opts.includeDirectories);
245
279
  pushFlag(args, "--gemini_dir", this.opts.geminiDir ?? this.opts.configDir);
246
- pushFlag(args, "--output-format", outputFormat);
247
280
  if (this.extraArgs?.length)
248
281
  args.push(...this.extraArgs);
249
282
  const systemPrefix = params.systemPrompt
@@ -253,14 +286,16 @@ export class AntigravityAgent extends BaseCliAgent {
253
286
  ? "\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
287
  : "";
255
288
  const fullPrompt = `${systemPrefix}${params.prompt ?? ""}${jsonReminder}`;
256
- args.push("--prompt", fullPrompt);
289
+ args.push("-p", fullPrompt);
257
290
  const accountEnv = {};
291
+ if (this.opts.geminiDir ?? this.opts.configDir)
292
+ accountEnv.GEMINI_DIR = this.opts.geminiDir ?? this.opts.configDir;
258
293
  if (this.opts.apiKey)
259
294
  accountEnv.GEMINI_API_KEY = this.opts.apiKey;
260
295
  return {
261
296
  command: this.opts.binary ?? "agy",
262
297
  args,
263
- outputFormat,
298
+ outputFormat: "text",
264
299
  env: Object.keys(accountEnv).length > 0 ? accountEnv : undefined,
265
300
  };
266
301
  }
@@ -1,30 +1,71 @@
1
1
  import type { BaseCliAgentOptions } from "./BaseCliAgent/BaseCliAgentOptions";
2
2
 
3
3
  export type AntigravityAgentOptions = BaseCliAgentOptions & {
4
- debug?: boolean;
5
4
  model?: string;
6
5
  sandbox?: boolean;
7
6
  yolo?: boolean;
8
7
  dangerouslySkipPermissions?: boolean;
9
8
  allowedMcpServerNames?: string[];
10
9
  allowedTools?: string[];
10
+ /**
11
+ * @deprecated Antigravity renamed extensions to plugins and manages them via
12
+ * `agy plugin`; launch-time extension flags are rejected at runtime.
13
+ */
11
14
  extensions?: string[];
15
+ /**
16
+ * @deprecated Use `agy plugin list` outside Smithers. This option is rejected
17
+ * at runtime because current `agy` builds no longer accept it during launch.
18
+ */
12
19
  listExtensions?: boolean;
20
+ /**
21
+ * Native Antigravity conversation id. Smithers emits `--conversation`.
22
+ */
23
+ conversation?: string;
24
+ /**
25
+ * Continue the latest Antigravity conversation. Smithers emits `--continue`.
26
+ */
27
+ continue?: boolean;
28
+ /**
29
+ * @deprecated Use `conversation`; Smithers still maps this to
30
+ * `--conversation` for compatibility.
31
+ */
13
32
  resume?: string;
33
+ /**
34
+ * @deprecated Conversation listing is interactive via `/resume`; this option
35
+ * is rejected at runtime.
36
+ */
14
37
  listSessions?: boolean;
38
+ /**
39
+ * @deprecated Conversation deletion is not a supported non-interactive
40
+ * launch flag; this option is rejected at runtime.
41
+ */
15
42
  deleteSession?: string;
16
43
  includeDirectories?: string[];
44
+ /**
45
+ * @deprecated Current `agy` builds do not expose `--screen-reader`; this
46
+ * option is rejected at runtime.
47
+ */
17
48
  screenReader?: boolean;
49
+ /**
50
+ * @deprecated Current `agy` builds do not expose `--output-format`; Smithers
51
+ * reads Antigravity stdout as text.
52
+ */
18
53
  outputFormat?: "text" | "json" | "stream-json";
54
+ /**
55
+ * @deprecated Current `agy` builds do not expose `--debug`; this option is
56
+ * rejected at runtime.
57
+ */
58
+ debug?: boolean;
19
59
  /**
20
60
  * Antigravity CLI binary to execute. The official CLI currently installs
21
61
  * `agy`; this exists for test harnesses and future binary renames.
22
62
  */
23
63
  binary?: string;
24
64
  /**
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/...`.
65
+ * Path to an isolated Google CLI config root. Smithers passes it as
66
+ * `--gemini_dir` and `GEMINI_DIR` so Antigravity reads/writes
67
+ * `<configDir>/antigravity-cli/...` instead of the user's default
68
+ * `~/.gemini/antigravity-cli/...`.
28
69
  */
29
70
  configDir?: string;
30
71
  /**
@@ -20,5 +20,17 @@ export type AgentGenerateOptions = {
20
20
  isRetry?: unknown;
21
21
  retryAttempt?: unknown;
22
22
  schemaRetry?: unknown;
23
+ /**
24
+ * Run context for the task this agent invocation belongs to. Surfaced to the
25
+ * spawned agent process (and its subprocesses) as SMITHERS_RUN_ID / NODE_ID /
26
+ * ITERATION / ATTEMPT so the agent can address its own run — e.g. to raise a
27
+ * blocking `smithers ask-human` request.
28
+ */
29
+ taskContext?: {
30
+ runId?: string;
31
+ nodeId?: string;
32
+ iteration?: number;
33
+ attempt?: number;
34
+ };
23
35
  [key: string]: unknown;
24
36
  };
@@ -14,6 +14,7 @@ import { extractTextFromJsonValue } from "./extractTextFromJsonValue.js";
14
14
  import { createAgentStdoutTextEmitter } from "./createAgentStdoutTextEmitter.js";
15
15
  import { buildGenerateResult } from "./buildGenerateResult.js";
16
16
  import { runCommandEffect } from "./runCommandEffect.js";
17
+ import { taskContextEnv } from "./taskContextEnv.js";
17
18
  /** @typedef {import("./AgentCliEvent.ts").AgentCliEvent} AgentCliEvent */
18
19
 
19
20
  /** @typedef {import("./AgentGenerateOptions.ts").AgentGenerateOptions} AgentGenerateOptions */
@@ -458,6 +459,7 @@ export function extractUsageFromOutput(raw) {
458
459
  const lines = stripOscSequences(raw).split(/\r?\n/).filter(Boolean);
459
460
  const usage = {};
460
461
  let found = false;
462
+ let countedIncremental = false;
461
463
  for (const line of lines) {
462
464
  let parsed;
463
465
  try {
@@ -480,6 +482,7 @@ export function extractUsageFromOutput(raw) {
480
482
  (usage.cacheWriteTokens ?? 0) + u.cache_creation_input_tokens;
481
483
  }
482
484
  found = true;
485
+ countedIncremental = true;
483
486
  continue;
484
487
  }
485
488
  if (parsed.type === "message_delta" && parsed.usage) {
@@ -488,8 +491,19 @@ export function extractUsageFromOutput(raw) {
488
491
  (usage.outputTokens ?? 0) + parsed.usage.output_tokens;
489
492
  }
490
493
  found = true;
494
+ countedIncremental = true;
491
495
  continue;
492
496
  }
497
+ if (parsed.type === "result") {
498
+ // Claude Code stream-json emits a terminal "result" event whose
499
+ // top-level usage summarizes tokens already accumulated from the
500
+ // per-message message_start/message_delta events. If we counted
501
+ // those incrementally, skip this event to avoid double-counting.
502
+ // Otherwise fall through so the usage is still captured.
503
+ if (countedIncremental) {
504
+ continue;
505
+ }
506
+ }
493
507
  if (parsed.type === "turn.completed" && parsed.usage) {
494
508
  const u = parsed.usage;
495
509
  if (u.input_tokens) {
@@ -615,7 +629,11 @@ export class BaseCliAgent {
615
629
  idleMs: this.idleTimeoutMs,
616
630
  });
617
631
  const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
618
- const env = { ...process.env, ...this.env };
632
+ const env = {
633
+ ...process.env,
634
+ ...this.env,
635
+ ...taskContextEnv(options?.taskContext),
636
+ };
619
637
  const combinedSystem = combineNonEmpty([
620
638
  this.systemPrompt,
621
639
  systemFromMessages,
@@ -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
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Map a task's run context into the `SMITHERS_*` environment variables that a
3
+ * Smithers-spawned agent — and any subprocess it runs, e.g. `smithers ask-human` —
4
+ * uses to identify the run/node it belongs to. Undefined/blank fields are omitted
5
+ * so we never clobber an inherited value with `"undefined"`.
6
+ *
7
+ * @param {{ runId?: string, nodeId?: string, iteration?: number, attempt?: number } | null | undefined} taskContext
8
+ * @returns {Record<string, string>}
9
+ */
10
+ export function taskContextEnv(taskContext) {
11
+ if (!taskContext) {
12
+ return {};
13
+ }
14
+ /** @type {Record<string, string>} */
15
+ const env = {};
16
+ if (typeof taskContext.runId === "string" && taskContext.runId.length > 0) {
17
+ env.SMITHERS_RUN_ID = taskContext.runId;
18
+ }
19
+ if (typeof taskContext.nodeId === "string" && taskContext.nodeId.length > 0) {
20
+ env.SMITHERS_NODE_ID = taskContext.nodeId;
21
+ }
22
+ if (typeof taskContext.iteration === "number" &&
23
+ Number.isInteger(taskContext.iteration)) {
24
+ env.SMITHERS_ITERATION = String(taskContext.iteration);
25
+ }
26
+ if (typeof taskContext.attempt === "number" &&
27
+ Number.isInteger(taskContext.attempt)) {
28
+ env.SMITHERS_ATTEMPT = String(taskContext.attempt);
29
+ }
30
+ return env;
31
+ }
@@ -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
  }
@@ -383,7 +383,7 @@ export class ClaudeCodeAgent extends BaseCliAgent {
383
383
  args.push("--chrome");
384
384
  if (this.opts.noChrome)
385
385
  args.push("--no-chrome");
386
- if (this.opts.continue)
386
+ if (this.opts.continue || params.options?.continueSession)
387
387
  args.push("--continue");
388
388
  if (this.opts.debug === true) {
389
389
  args.push("--debug");
@@ -445,9 +445,27 @@ export class ClaudeCodeAgent extends BaseCliAgent {
445
445
  args.push("--verbose");
446
446
  if (this.extraArgs?.length)
447
447
  args.push(...this.extraArgs);
448
+ // Durability: inject a PostToolUse hook that calls back into smithers for a
449
+ // strict Tier 1 snapshot at each file-edit / Bash boundary. Only when the
450
+ // engine passes a socket path (durability enabled); additive --settings.
451
+ const durabilitySocket = typeof params.options?.durabilitySocket === "string"
452
+ ? params.options.durabilitySocket
453
+ : undefined;
454
+ if (durabilitySocket) {
455
+ args.push("--settings", JSON.stringify({
456
+ hooks: {
457
+ PostToolUse: [{
458
+ matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
459
+ hooks: [{ type: "command", command: "smithers snapshot-hook" }],
460
+ }],
461
+ },
462
+ }));
463
+ }
448
464
  if (params.prompt)
449
465
  args.push(params.prompt);
450
466
  const accountEnv = {};
467
+ if (durabilitySocket)
468
+ accountEnv.SMITHERS_SNAPSHOT_SOCK = durabilitySocket;
451
469
  if (this.opts.configDir)
452
470
  accountEnv.CLAUDE_CONFIG_DIR = this.opts.configDir;
453
471
  if (this.opts.apiKey)
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
  : "";
package/src/ForgeAgent.js CHANGED
@@ -5,6 +5,31 @@ import { randomUUID } from "node:crypto";
5
5
  /** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
6
6
  /** @typedef {import("./ForgeAgentOptions.ts").ForgeAgentOptions} ForgeAgentOptions */
7
7
 
8
+ /**
9
+ * @returns {AgentCapabilityRegistry}
10
+ */
11
+ export function createForgeCapabilityRegistry() {
12
+ return {
13
+ version: 1,
14
+ engine: "forge",
15
+ runtimeTools: {},
16
+ mcp: {
17
+ bootstrap: "unsupported",
18
+ supportsProjectScope: false,
19
+ supportsUserScope: false,
20
+ },
21
+ skills: {
22
+ supportsSkills: false,
23
+ smithersSkillIds: [],
24
+ },
25
+ humanInteraction: {
26
+ supportsUiRequests: false,
27
+ methods: [],
28
+ },
29
+ builtIns: ["default"],
30
+ };
31
+ }
32
+
8
33
  export class ForgeAgent extends BaseCliAgent {
9
34
  opts;
10
35
  /** @type {AgentCapabilityRegistry} */
@@ -17,25 +42,7 @@ export class ForgeAgent extends BaseCliAgent {
17
42
  constructor(opts = {}) {
18
43
  super(opts);
19
44
  this.opts = opts;
20
- this.capabilities = {
21
- version: 1,
22
- engine: "forge",
23
- runtimeTools: {},
24
- mcp: {
25
- bootstrap: "unsupported",
26
- supportsProjectScope: false,
27
- supportsUserScope: false,
28
- },
29
- skills: {
30
- supportsSkills: false,
31
- smithersSkillIds: [],
32
- },
33
- humanInteraction: {
34
- supportsUiRequests: false,
35
- methods: [],
36
- },
37
- builtIns: ["default"],
38
- };
45
+ this.capabilities = createForgeCapabilityRegistry();
39
46
  }
40
47
  /**
41
48
  * @returns {CliOutputInterpreter}
@@ -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 || baseURL.trim() === "") {
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
+ }