@smithers-orchestrator/agents 0.23.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.23.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",
@@ -51,9 +56,9 @@
51
56
  "ai": "^6.0.168",
52
57
  "effect": "^3.21.1",
53
58
  "zod": "^4.3.6",
54
- "@smithers-orchestrator/errors": "0.23.0",
55
- "@smithers-orchestrator/driver": "0.23.0",
56
- "@smithers-orchestrator/observability": "0.23.0"
59
+ "@smithers-orchestrator/driver": "0.24.0",
60
+ "@smithers-orchestrator/errors": "0.24.0",
61
+ "@smithers-orchestrator/observability": "0.24.0"
57
62
  },
58
63
  "devDependencies": {
59
64
  "@types/bun": "latest",
@@ -451,6 +451,27 @@ function buildStreamResult(result) {
451
451
  fullStream: fullStream,
452
452
  };
453
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
+ }
454
475
  /**
455
476
  * @param {string} raw
456
477
  * @returns {CliUsageInfo | undefined}
@@ -851,6 +872,10 @@ export class BaseCliAgent {
851
872
  idleTimeoutMs: callTimeouts.idleMs,
852
873
  signal: options?.abortSignal,
853
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",
854
879
  onStdout: (chunk) => {
855
880
  stdoutEmitter?.push(chunk);
856
881
  handleInterpreterChunk("stdout", chunk);
@@ -863,12 +888,30 @@ export class BaseCliAgent {
863
888
  flushBufferedLines("stdout", true);
864
889
  flushBufferedLines("stderr", true);
865
890
  emitEvents(interpreter?.onExit?.(result));
866
- 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
867
909
  ? yield* Effect.tryPromise({
868
910
  try: () => fs.readFile(commandSpec.outputFile, "utf8"),
869
911
  catch: (cause) => toSmithersError(cause, "read output file"),
870
- }).pipe(Effect.catchAll(() => Effect.succeed(result.stdout)))
871
- : result.stdout;
912
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)))
913
+ : null;
914
+ const stdout = typeof outputFileText === "string" ? outputFileText : result.stdout;
872
915
  if (result.exitCode && result.exitCode !== 0) {
873
916
  const filteredStderr = filterBenignStderr(result.stderr, commandSpec.benignStderrPatterns);
874
917
  if (!(commandSpec.command === "codex" && filteredStderr.length === 0)) {
@@ -934,13 +977,35 @@ export class BaseCliAgent {
934
977
  }
935
978
  }
936
979
  }
937
- const extractedText = outputFormat === "json" || outputFormat === "stream-json"
938
- ? (extractTextFromJsonPayload(rawText) ?? rawText)
980
+ const extractedFromStdout = outputFormat === "json" || outputFormat === "stream-json"
981
+ ? extractTextFromJsonPayload(rawText)
939
982
  : rawText;
940
- 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);
941
1006
  // Extract token usage from raw stdout before text extraction strips it.
942
1007
  // Each CLI harness embeds usage differently (NDJSON events, JSON stats, etc.)
943
- const cliUsage = extractUsageFromOutput(stdout);
1008
+ const cliUsage = extractUsageFromOutput(stdout) ?? usageFromCompletedEvent(completedEvent);
944
1009
  const usage = cliUsage ? {
945
1010
  inputTokens: cliUsage.inputTokens,
946
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({
@@ -113,7 +113,7 @@ const claudeRateLimitCheck = {
113
113
  "content-type": "application/json",
114
114
  },
115
115
  body: JSON.stringify({
116
- model: "claude-sonnet-4-20250514",
116
+ model: "claude-fable-5",
117
117
  messages: [{ role: "user", content: "hi" }],
118
118
  }),
119
119
  signal: AbortSignal.timeout(4_000),
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Options for shaping the generated toolset. Mirrors the curation knobs on
3
+ * `createOpenApiTools` so MCP and OpenAPI integrations feel the same.
4
+ */
5
+ export type McpToolsetOptions = {
6
+ /** Only expose these MCP tool names. */
7
+ include?: string[];
8
+ /** Drop these MCP tool names. */
9
+ exclude?: string[];
10
+ /** Prefix applied to every tool name, e.g. `"github_"`. */
11
+ namePrefix?: string;
12
+ /** Identifies this client to the server. */
13
+ clientName?: string;
14
+ /** Client version reported to the server. */
15
+ clientVersion?: string;
16
+ };
@@ -0,0 +1,12 @@
1
+ import type { McpServerConfig } from "./McpServerConfig.js";
2
+ import type { McpToolset } from "./McpToolset.js";
3
+ import type { McpToolsetOptions } from "./McpToolsetOptions.js";
4
+
5
+ export type { McpServerConfig } from "./McpServerConfig.js";
6
+ export type { McpToolset } from "./McpToolset.js";
7
+ export type { McpToolsetOptions } from "./McpToolsetOptions.js";
8
+
9
+ export declare function createMcpToolset(
10
+ config: McpServerConfig,
11
+ options?: McpToolsetOptions,
12
+ ): Promise<McpToolset>;
@@ -4,18 +4,7 @@ import { dynamicTool, jsonSchema } from "ai";
4
4
 
5
5
  /** @typedef {import("./McpServerConfig.ts").McpServerConfig} McpServerConfig */
6
6
  /** @typedef {import("./McpToolset.ts").McpToolset} McpToolset */
7
-
8
- /**
9
- * Options for shaping the generated toolset. Mirrors the curation knobs on
10
- * `createOpenApiTools` so MCP and OpenAPI integrations feel the same.
11
- *
12
- * @typedef {object} McpToolsetOptions
13
- * @property {string[]} [include] Only expose these MCP tool names.
14
- * @property {string[]} [exclude] Drop these MCP tool names.
15
- * @property {string} [namePrefix] Prefix applied to every tool name (e.g. `"github_"`).
16
- * @property {string} [clientName] Identifies this client to the server.
17
- * @property {string} [clientVersion] Client version reported to the server.
18
- */
7
+ /** @typedef {import("./McpToolsetOptions.ts").McpToolsetOptions} McpToolsetOptions */
19
8
 
20
9
  /**
21
10
  * Connect to an MCP server and expose its tools as AI SDK tools an agent can call.