@smithers-orchestrator/agents 0.22.0 → 0.24.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 +10 -4
  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 +78 -8
  7. package/src/BaseCliAgent/RunCommandResult.ts +4 -0
  8. package/src/BaseCliAgent/runCommandEffect.js +3 -2
  9. package/src/BaseCliAgent/taskContextEnv.js +31 -0
  10. package/src/ClaudeCodeAgent.js +19 -1
  11. package/src/ForgeAgent.js +26 -19
  12. package/src/HermesAgent.js +1 -1
  13. package/src/VibeAgent.js +214 -0
  14. package/src/VibeAgentOptions.ts +11 -0
  15. package/src/agent-contract/createSmithersAgentContract.js +1 -0
  16. package/src/agent-contract/renderSmithersAgentPromptGuidance.js +4 -0
  17. package/src/capability-registry/AgentCapabilityRegistry.ts +1 -1
  18. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +4 -1
  19. package/src/cli-capabilities/CliAgentCapabilityReportEntry.ts +2 -0
  20. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +48 -1
  21. package/src/cli-capabilities/getCliAgentCapabilityReport.js +24 -0
  22. package/src/cli-capabilities/index.js +5 -0
  23. package/src/cli-surface/CliAgentSurfaceTypes.ts +34 -0
  24. package/src/cli-surface/cliAgentSurfaceManifest.js +490 -0
  25. package/src/cli-surface/index.js +5 -0
  26. package/src/diagnostics/getDiagnosticStrategy.js +1 -1
  27. package/src/index.d.ts +707 -386
  28. package/src/index.js +21 -0
  29. package/src/mcp/McpServerConfig.ts +19 -0
  30. package/src/mcp/McpToolset.ts +17 -0
  31. package/src/mcp/McpToolsetOptions.ts +16 -0
  32. package/src/mcp/createMcpToolset.d.ts +12 -0
  33. package/src/mcp/createMcpToolset.js +83 -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.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "AI SDK and CLI agent adapters for Smithers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -35,6 +35,11 @@
35
35
  "import": "./src/cli-capabilities/index.js",
36
36
  "default": "./src/cli-capabilities/index.js"
37
37
  },
38
+ "./mcp/createMcpToolset": {
39
+ "types": "./src/mcp/createMcpToolset.d.ts",
40
+ "import": "./src/mcp/createMcpToolset.js",
41
+ "default": "./src/mcp/createMcpToolset.js"
42
+ },
38
43
  "./*": {
39
44
  "types": "./src/index.d.ts",
40
45
  "import": "./src/*.js",
@@ -47,12 +52,13 @@
47
52
  "dependencies": {
48
53
  "@ai-sdk/anthropic": "^3.0.71",
49
54
  "@ai-sdk/openai": "^3.0.53",
55
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
56
  "ai": "^6.0.168",
51
57
  "effect": "^3.21.1",
52
58
  "zod": "^4.3.6",
53
- "@smithers-orchestrator/driver": "0.22.0",
54
- "@smithers-orchestrator/errors": "0.22.0",
55
- "@smithers-orchestrator/observability": "0.22.0"
59
+ "@smithers-orchestrator/driver": "0.24.0",
60
+ "@smithers-orchestrator/errors": "0.24.0",
61
+ "@smithers-orchestrator/observability": "0.24.0"
56
62
  },
57
63
  "devDependencies": {
58
64
  "@types/bun": "latest",
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 */
@@ -450,6 +451,27 @@ function buildStreamResult(result) {
450
451
  fullStream: fullStream,
451
452
  };
452
453
  }
454
+ /**
455
+ * Fallback when truncated stdout lost the per-message usage events: the
456
+ * interpreter's completed event carries the harness usage summary (#277).
457
+ * @param {{ usage?: unknown } | null} completedEvent
458
+ * @returns {CliUsageInfo | undefined}
459
+ */
460
+ function usageFromCompletedEvent(completedEvent) {
461
+ const u = completedEvent?.usage;
462
+ if (!u || typeof u !== "object" || Array.isArray(u))
463
+ return undefined;
464
+ const num = (value) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
465
+ const usage = {
466
+ inputTokens: num(u.input_tokens) ?? num(u.inputTokens),
467
+ outputTokens: num(u.output_tokens) ?? num(u.outputTokens),
468
+ cacheReadTokens: num(u.cache_read_input_tokens) ?? num(u.cacheReadTokens),
469
+ cacheWriteTokens: num(u.cache_creation_input_tokens) ?? num(u.cacheWriteTokens),
470
+ reasoningTokens: num(u.reasoning_tokens) ?? num(u.reasoningTokens),
471
+ totalTokens: num(u.total_tokens) ?? num(u.totalTokens),
472
+ };
473
+ return Object.values(usage).some((value) => value !== undefined) ? usage : undefined;
474
+ }
453
475
  /**
454
476
  * @param {string} raw
455
477
  * @returns {CliUsageInfo | undefined}
@@ -628,7 +650,11 @@ export class BaseCliAgent {
628
650
  idleMs: this.idleTimeoutMs,
629
651
  });
630
652
  const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
631
- const env = { ...process.env, ...this.env };
653
+ const env = {
654
+ ...process.env,
655
+ ...this.env,
656
+ ...taskContextEnv(options?.taskContext),
657
+ };
632
658
  const combinedSystem = combineNonEmpty([
633
659
  this.systemPrompt,
634
660
  systemFromMessages,
@@ -846,6 +872,10 @@ export class BaseCliAgent {
846
872
  idleTimeoutMs: callTimeouts.idleMs,
847
873
  signal: options?.abortSignal,
848
874
  maxOutputBytes: this.maxOutputBytes ?? options?.maxOutputBytes,
875
+ // CLI harnesses emit their final result event at the END of
876
+ // the stream; if the capture cap trips, the tail is the part
877
+ // that must survive (#277).
878
+ truncateKeep: "tail",
849
879
  onStdout: (chunk) => {
850
880
  stdoutEmitter?.push(chunk);
851
881
  handleInterpreterChunk("stdout", chunk);
@@ -858,12 +888,30 @@ export class BaseCliAgent {
858
888
  flushBufferedLines("stdout", true);
859
889
  flushBufferedLines("stderr", true);
860
890
  emitEvents(interpreter?.onExit?.(result));
861
- const stdout = commandSpec.outputFile
891
+ if (result.stdoutTruncated) {
892
+ emitEvents({
893
+ type: "action",
894
+ engine: commandSpec.command,
895
+ phase: "completed",
896
+ entryType: "thought",
897
+ action: {
898
+ id: `stdout-truncated-${randomUUID()}`,
899
+ kind: "warning",
900
+ title: "captured stdout truncated",
901
+ detail: {},
902
+ },
903
+ message: "Captured stdout exceeded maxOutputBytes; kept the stream tail. The streamed interpreter answer is used as the result text.",
904
+ ok: true,
905
+ level: "warning",
906
+ });
907
+ }
908
+ const outputFileText = commandSpec.outputFile
862
909
  ? yield* Effect.tryPromise({
863
910
  try: () => fs.readFile(commandSpec.outputFile, "utf8"),
864
911
  catch: (cause) => toSmithersError(cause, "read output file"),
865
- }).pipe(Effect.catchAll(() => Effect.succeed(result.stdout)))
866
- : result.stdout;
912
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)))
913
+ : null;
914
+ const stdout = typeof outputFileText === "string" ? outputFileText : result.stdout;
867
915
  if (result.exitCode && result.exitCode !== 0) {
868
916
  const filteredStderr = filterBenignStderr(result.stderr, commandSpec.benignStderrPatterns);
869
917
  if (!(commandSpec.command === "codex" && filteredStderr.length === 0)) {
@@ -929,13 +977,35 @@ export class BaseCliAgent {
929
977
  }
930
978
  }
931
979
  }
932
- const extractedText = outputFormat === "json" || outputFormat === "stream-json"
933
- ? (extractTextFromJsonPayload(rawText) ?? rawText)
980
+ const extractedFromStdout = outputFormat === "json" || outputFormat === "stream-json"
981
+ ? extractTextFromJsonPayload(rawText)
934
982
  : rawText;
935
- const output = tryParseJson(extractedText);
983
+ // The interpreter parses the live stream line-by-line BEFORE the
984
+ // capture cap applies, so its completed answer survives stdout
985
+ // truncation. Prefer it whenever the captured stdout was
986
+ // truncated or yields no final message; otherwise keep the
987
+ // historical extraction so intact runs are unchanged (#277).
988
+ const streamedAnswer = typeof completedEvent?.answer === "string" && completedEvent.answer.trim().length > 0
989
+ ? completedEvent.answer
990
+ : undefined;
991
+ // A dedicated final-message file (e.g. codex --output-last-message)
992
+ // is the CLI's authoritative output channel: it holds the complete
993
+ // final message and is immune to the stdout byte cap and to
994
+ // line-by-line stream interpretation. When it parsed as JSON, trust
995
+ // it over the truncation/stream fallbacks, which otherwise surface a
996
+ // short `message` field instead of the full structured object.
997
+ const outputFileJson = typeof outputFileText === "string" && outputFileText.trim() !== ""
998
+ ? tryParseJson(outputFileText)
999
+ : null;
1000
+ const extractedText = outputFileJson != null
1001
+ ? outputFileText
1002
+ : result.stdoutTruncated || extractedFromStdout == null || extractedFromStdout.trim() === ""
1003
+ ? (streamedAnswer ?? extractedFromStdout ?? rawText)
1004
+ : extractedFromStdout;
1005
+ const output = outputFileJson ?? tryParseJson(extractedText);
936
1006
  // Extract token usage from raw stdout before text extraction strips it.
937
1007
  // Each CLI harness embeds usage differently (NDJSON events, JSON stats, etc.)
938
- const cliUsage = extractUsageFromOutput(stdout);
1008
+ const cliUsage = extractUsageFromOutput(stdout) ?? usageFromCompletedEvent(completedEvent);
939
1009
  const usage = cliUsage ? {
940
1010
  inputTokens: cliUsage.inputTokens,
941
1011
  inputTokenDetails: {
@@ -2,4 +2,8 @@ export type RunCommandResult = {
2
2
  stdout: string;
3
3
  stderr: string;
4
4
  exitCode: number | null;
5
+ /** True when captured stdout exceeded maxOutputBytes and was truncated. */
6
+ stdoutTruncated?: boolean;
7
+ /** True when captured stderr exceeded maxOutputBytes and was truncated. */
8
+ stderrTruncated?: boolean;
5
9
  };
@@ -1,7 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
  import { spawnCaptureEffect } from "@smithers-orchestrator/driver/child-process";
3
3
  /**
4
- * @typedef {{ cwd: string; env: Record<string, string>; input?: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; }} RunCommandOptions
4
+ * @typedef {{ cwd: string; env: Record<string, string>; input?: string; timeoutMs?: number; idleTimeoutMs?: number; signal?: AbortSignal; maxOutputBytes?: number; truncateKeep?: "head" | "tail"; onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void; }} RunCommandOptions
5
5
  */
6
6
  /** @typedef {import("./RunCommandResult.ts").RunCommandResult} RunCommandResult */
7
7
  /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
@@ -13,7 +13,7 @@ import { spawnCaptureEffect } from "@smithers-orchestrator/driver/child-process"
13
13
  * @returns {Effect.Effect<RunCommandResult, SmithersError>}
14
14
  */
15
15
  export function runCommandEffect(command, args, options) {
16
- const { cwd, env, input, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, onStdout, onStderr, } = options;
16
+ const { cwd, env, input, timeoutMs, idleTimeoutMs, signal, maxOutputBytes, truncateKeep, onStdout, onStderr, } = options;
17
17
  return spawnCaptureEffect(command, args, {
18
18
  cwd,
19
19
  env,
@@ -22,6 +22,7 @@ export function runCommandEffect(command, args, options) {
22
22
  timeoutMs,
23
23
  idleTimeoutMs,
24
24
  maxOutputBytes,
25
+ truncateKeep,
25
26
  onStdout,
26
27
  onStderr,
27
28
  }).pipe(Effect.annotateLogs({
@@ -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
+ }
@@ -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/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}
@@ -29,7 +29,7 @@ export class HermesAgent extends OpenAIAgent {
29
29
  nativeStructuredOutput = false,
30
30
  ...rest
31
31
  } = opts;
32
- if (baseURL === undefined) {
32
+ if (baseURL === undefined || baseURL.trim() === "") {
33
33
  throw new SmithersError(
34
34
  "AGENT_CONFIG_INVALID",
35
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.",