@smithers-orchestrator/agents 0.24.2 → 0.25.1

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 (55) hide show
  1. package/package.json +15 -5
  2. package/src/AgentLike.ts +5 -0
  3. package/src/AmpAgent.js +15 -5
  4. package/src/AmpAgentOptions.ts +6 -0
  5. package/src/BaseCliAgent/BaseCliAgent.js +198 -10
  6. package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +21 -3
  7. package/src/BaseCliAgent/index.d.ts +467 -0
  8. package/src/ClaudeCodeAgent.js +6 -2
  9. package/src/CodexAgent.js +4 -0
  10. package/src/GeminiAgent.js +34 -224
  11. package/src/GeminiAgentOptions.ts +4 -9
  12. package/src/OpenCodeAgent.js +2 -12
  13. package/src/OpenCodeAgentOptions.ts +19 -0
  14. package/src/PiAgent.js +4 -0
  15. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +0 -1
  16. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +3 -2
  17. package/src/cli-capabilities/getCliAgentCapabilityReport.js +0 -6
  18. package/src/cli-surface/cliAgentSurfaceManifest.js +1 -40
  19. package/src/createElevenLabsTextToSpeechTool.js +128 -0
  20. package/src/createElevenLabsTextToSpeechTool.ts +33 -0
  21. package/src/diagnostics/getDiagnosticStrategy.js +163 -35
  22. package/src/document-parsing/DocumentParsingProvider.ts +13 -0
  23. package/src/document-parsing/DocumentParsingResult.ts +13 -0
  24. package/src/document-parsing/DocumentParsingToolset.ts +4 -0
  25. package/src/document-parsing/DocumentParsingToolsetOptions.ts +9 -0
  26. package/src/document-parsing/createDocumentParsingToolset.d.ts +9 -0
  27. package/src/document-parsing/createDocumentParsingToolset.js +416 -0
  28. package/src/http/CreateHttpToolOptions.ts +4 -0
  29. package/src/http/HttpToolAuth.ts +15 -0
  30. package/src/http/HttpToolInput.ts +11 -0
  31. package/src/http/HttpToolOutput.ts +7 -0
  32. package/src/http/createHttpTool.js +136 -0
  33. package/src/image-generation/ImageGenerationProvider.ts +7 -0
  34. package/src/image-generation/ImageGenerationRequest.ts +8 -0
  35. package/src/image-generation/ImageGenerationResult.ts +10 -0
  36. package/src/image-generation/ImageGenerationToolOptions.ts +10 -0
  37. package/src/image-generation/createImageGenerationTool.d.ts +18 -0
  38. package/src/image-generation/createImageGenerationTool.js +92 -0
  39. package/src/index.d.ts +490 -147
  40. package/src/index.js +23 -5
  41. package/src/streamResultToGenerateResult.js +55 -26
  42. package/src/transcription/createTranscriptionTool.js +182 -0
  43. package/src/transcription/createTranscriptionTool.ts +29 -0
  44. package/src/transcription/index.js +1 -0
  45. package/src/transcription/index.ts +6 -0
  46. package/src/web-search/GroundedWebSearchProvider.ts +21 -0
  47. package/src/web-search/GroundedWebSearchToolset.ts +6 -0
  48. package/src/web-search/createBraveSearchProvider.js +53 -0
  49. package/src/web-search/createExaSearchProvider.js +72 -0
  50. package/src/web-search/createGroundedWebSearchToolset.js +110 -0
  51. package/src/web-search/createSerperSearchProvider.js +63 -0
  52. package/src/web-search/createTavilySearchProvider.js +59 -0
  53. package/src/web-search/index.js +5 -0
  54. package/src/zodToOpenAISchema.js +4 -0
  55. package/src/OpenCodeAgent.ts +0 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "description": "AI SDK and CLI agent adapters for Smithers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -11,7 +11,7 @@
11
11
  "default": "./src/index.js"
12
12
  },
13
13
  "./BaseCliAgent": {
14
- "types": "./src/index.d.ts",
14
+ "types": "./src/BaseCliAgent/index.d.ts",
15
15
  "import": "./src/BaseCliAgent/index.js",
16
16
  "default": "./src/BaseCliAgent/index.js"
17
17
  },
@@ -40,6 +40,16 @@
40
40
  "import": "./src/mcp/createMcpToolset.js",
41
41
  "default": "./src/mcp/createMcpToolset.js"
42
42
  },
43
+ "./document-parsing/createDocumentParsingToolset": {
44
+ "types": "./src/document-parsing/createDocumentParsingToolset.d.ts",
45
+ "import": "./src/document-parsing/createDocumentParsingToolset.js",
46
+ "default": "./src/document-parsing/createDocumentParsingToolset.js"
47
+ },
48
+ "./image-generation/createImageGenerationTool": {
49
+ "types": "./src/image-generation/createImageGenerationTool.d.ts",
50
+ "import": "./src/image-generation/createImageGenerationTool.js",
51
+ "default": "./src/image-generation/createImageGenerationTool.js"
52
+ },
43
53
  "./*": {
44
54
  "types": "./src/index.d.ts",
45
55
  "import": "./src/*.js",
@@ -56,9 +66,9 @@
56
66
  "ai": "^6.0.168",
57
67
  "effect": "^3.21.1",
58
68
  "zod": "^4.3.6",
59
- "@smithers-orchestrator/driver": "0.24.2",
60
- "@smithers-orchestrator/errors": "0.24.2",
61
- "@smithers-orchestrator/observability": "0.24.2"
69
+ "@smithers-orchestrator/driver": "0.25.1",
70
+ "@smithers-orchestrator/errors": "0.25.1",
71
+ "@smithers-orchestrator/observability": "0.25.1"
62
72
  },
63
73
  "devDependencies": {
64
74
  "@types/bun": "latest",
package/src/AgentLike.ts CHANGED
@@ -14,6 +14,11 @@ export type AgentLike = {
14
14
  capabilities?: AgentCapabilityRegistry;
15
15
  /** True when the agent consumes outputSchema through a native structured-output API. */
16
16
  supportsNativeStructuredOutput?: boolean;
17
+ /**
18
+ * Performs deterministic startup checks before the first generation call in a
19
+ * workflow run. A rejected promise fails the task without retrying.
20
+ */
21
+ preflight?: (args?: AgentGenerateOptions) => Promise<void>;
17
22
  /**
18
23
  * Generates a response or action based on the provided arguments.
19
24
  *
package/src/AmpAgent.js CHANGED
@@ -194,7 +194,15 @@ export class AmpAgent extends BaseCliAgent {
194
194
  * @param {{ prompt: string; systemPrompt?: string; cwd: string; options: any; }} params
195
195
  */
196
196
  async buildCommand(params) {
197
- const args = [];
197
+ // Resume an existing thread when a session id is provided. Amp continues a
198
+ // thread with `amp threads continue <id>` (vs starting a fresh thread), so
199
+ // new-thread-only flags (--visibility, --archive) are skipped on resume.
200
+ // NB: the `threads continue` invocation follows the Amp manifest; confirm
201
+ // against the live `amp` CLI if its thread syntax changes.
202
+ const resumeSession = typeof params.options?.resumeSession === "string"
203
+ ? params.options.resumeSession
204
+ : this.opts.resume;
205
+ const args = resumeSession ? ["threads", "continue", resumeSession] : [];
198
206
  const yoloEnabled = this.opts.yolo ?? this.yolo;
199
207
  // Dangerous allow all (yolo mode) — must come before --execute
200
208
  if (this.opts.dangerouslyAllowAll || yoloEnabled) {
@@ -202,8 +210,9 @@ export class AmpAgent extends BaseCliAgent {
202
210
  }
203
211
  // Model / mode
204
212
  pushFlag(args, "--model", this.opts.model ?? this.model);
205
- // Visibility for new threads
206
- pushFlag(args, "--visibility", this.opts.visibility);
213
+ // Visibility for new threads (not applicable when continuing one)
214
+ if (!resumeSession)
215
+ pushFlag(args, "--visibility", this.opts.visibility);
207
216
  // MCP config
208
217
  pushFlag(args, "--mcp-config", this.opts.mcpConfig);
209
218
  // Settings file
@@ -217,8 +226,9 @@ export class AmpAgent extends BaseCliAgent {
217
226
  args.push("--no-jetbrains");
218
227
  // Color handling
219
228
  args.push("--no-color");
220
- // Archive thread after execution to keep things clean
221
- args.push("--archive");
229
+ // Archive thread after execution to keep things clean (new threads only)
230
+ if (!resumeSession)
231
+ args.push("--archive");
222
232
  if (this.extraArgs?.length)
223
233
  args.push(...this.extraArgs);
224
234
  // Build prompt with system prompt prepended
@@ -4,6 +4,12 @@ import type { BaseCliAgentOptions } from "./BaseCliAgent/BaseCliAgentOptions";
4
4
  * Configuration options for the AmpAgent.
5
5
  */
6
6
  export type AmpAgentOptions = BaseCliAgentOptions & {
7
+ /**
8
+ * Thread id to continue. When set (or when a task passes
9
+ * `options.resumeSession`), buildCommand emits `amp threads continue <id>`
10
+ * instead of starting a fresh thread.
11
+ */
12
+ resume?: string;
7
13
  /** Visibility setting for the new thread (e.g., private, public) */
8
14
  visibility?: "private" | "public" | "workspace" | "group";
9
15
  /** Path to a specific MCP configuration file */
@@ -5,6 +5,68 @@ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
5
5
  import { logDebug, logInfo, logWarning } from "@smithers-orchestrator/observability/logging";
6
6
  import { agentDurationMs, agentErrorsTotal, agentInvocationsTotal, agentRetriesTotal, agentTokensTotal, } from "@smithers-orchestrator/observability/metrics";
7
7
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
8
+
9
+ const QUOTA_PATTERNS = [
10
+ /\bhit\s+your\s+usage\s+limit\b/i,
11
+ /\busage\s+limit\s+exceeded\b/i,
12
+ /\bquota\s+exceeded\b/i,
13
+ /\brate\s+limit\s+exceeded\b/i,
14
+ /\byou('ve| have)\s+reached\s+(your\s+)?(usage|rate|quota)\b/i,
15
+ /\b(usage|quota|rate)\s+(cap|ceiling|limit)\s+(reached|exceeded|hit)\b/i,
16
+ /\btoo\s+many\s+requests\b/i,
17
+ /\b(429|rate.limit)\b[\s\S]{0,100}?try\s+again\b/i,
18
+ ];
19
+
20
+ /**
21
+ * Detects provider quota/rate-limit errors and returns a SmithersError with
22
+ * AGENT_QUOTA_EXCEEDED code. The reset time is parsed when present.
23
+ *
24
+ * @param {string} message
25
+ * @param {string} command
26
+ * @param {{ agentId?: string; agentModel?: string; agentEngine?: string; nowMs?: () => number }} [context]
27
+ * @returns {SmithersError | null}
28
+ */
29
+ export function classifyQuotaError(message, command, context = {}) {
30
+ if (!message) return null;
31
+ const isQuota = QUOTA_PATTERNS.some((re) => re.test(message));
32
+ if (!isQuota) return null;
33
+ const { agentId, agentModel, agentEngine, nowMs = () => Date.now() } = context;
34
+ const now = nowMs();
35
+ let quotaResetAtMs;
36
+ let resetHint;
37
+ // Format: "try again at Jun 18th, 2026 9:54 AM" — strip ordinal suffix before parsing
38
+ const dateMatch = /try again at\s+([A-Z][a-z]+ \d+(?:st|nd|rd|th)?,?\s+\d{4}\s+\d+:\d+\s+(?:AM|PM))/i.exec(message);
39
+ if (dateMatch) {
40
+ const normalized = dateMatch[1].replace(/(\d+)(st|nd|rd|th)\b/gi, "$1");
41
+ const parsed = Date.parse(normalized);
42
+ if (Number.isFinite(parsed) && parsed > now) {
43
+ quotaResetAtMs = parsed;
44
+ }
45
+ resetHint = dateMatch[0];
46
+ } else {
47
+ // Format: "retry after N seconds"
48
+ const secondsMatch = /retry after\s+(\d+)\s+second/i.exec(message);
49
+ if (secondsMatch) {
50
+ const deltaMs = Number(secondsMatch[1]) * 1000;
51
+ if (deltaMs > 0) {
52
+ quotaResetAtMs = now + deltaMs;
53
+ }
54
+ resetHint = secondsMatch[0];
55
+ }
56
+ }
57
+ const modelLabel = agentModel ?? "<unset>";
58
+ const idLabel = agentId ?? "<anonymous>";
59
+ const summary = `Agent "${idLabel}" (${command}, model=${modelLabel}) hit a provider usage/quota limit: ${message.slice(0, 300)}.${resetHint ? ` ${resetHint}.` : ""} Retries are preserved; the run will pause until the quota resets.`;
60
+ return new SmithersError("AGENT_QUOTA_EXCEEDED", summary, {
61
+ failureQuota: true,
62
+ agentId: idLabel,
63
+ agentEngine: agentEngine ?? "unknown",
64
+ agentModel: modelLabel,
65
+ command,
66
+ underlying: message.slice(0, 500),
67
+ ...(quotaResetAtMs != null ? { quotaResetAtMs } : {}),
68
+ });
69
+ }
8
70
  import { launchDiagnostics, enrichReportWithErrorAnalysis, formatDiagnosticSummary } from "../diagnostics/index.js";
9
71
  import { extractPrompt } from "./extractPrompt.js";
10
72
  import { resolveTimeouts } from "./resolveTimeouts.js";
@@ -299,6 +361,8 @@ function extractTextFromJsonPayload(raw) {
299
361
  }
300
362
  const chunks = [];
301
363
  for (const parsed of parsedLines) {
364
+ if (typeof parsed?.role === "string" && parsed.role !== "assistant")
365
+ continue;
302
366
  let text;
303
367
  if (parsed?.type === "text" && typeof parsed?.part?.text === "string") {
304
368
  text = parsed.part.text;
@@ -692,15 +756,8 @@ export class BaseCliAgent {
692
756
  const agentId = this.id;
693
757
  const agentModel = this.model;
694
758
  const agentEngine = resolveAgentEngineTag(this);
695
- /**
696
- * Detect well-known non-retryable CLI agent configuration errors so the
697
- * engine surfaces them with a clear, actionable message and stops retrying
698
- * (these errors are deterministic and will never recover by re-running).
699
- *
700
- * @param {string} message
701
- * @param {string} command
702
- * @returns {SmithersError | null}
703
- */
759
+ const agentCtx = { agentId, agentModel, agentEngine };
760
+ const classifyQuota = (message, command) => classifyQuotaError(message, command, agentCtx);
704
761
  function classifyNonRetryableAgentError(message, command) {
705
762
  if (!message)
706
763
  return null;
@@ -922,6 +979,10 @@ export class BaseCliAgent {
922
979
  filteredStderr ||
923
980
  result.stdout.trim() ||
924
981
  `CLI exited with code ${result.exitCode}`;
982
+ const quota = classifyQuota(errorText, commandSpec.command);
983
+ if (quota) {
984
+ return yield* Effect.fail(quota);
985
+ }
925
986
  const nonRetryable = classifyNonRetryableAgentError(errorText, commandSpec.command);
926
987
  if (nonRetryable) {
927
988
  return yield* Effect.fail(nonRetryable);
@@ -949,7 +1010,12 @@ export class BaseCliAgent {
949
1010
  }
950
1011
  }
951
1012
  if (completedEvent?.ok === false) {
952
- return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", completedEvent.error || "CLI agent reported an error"));
1013
+ const completedError = completedEvent.error || "CLI agent reported an error";
1014
+ const completedQuota = classifyQuota(completedError, commandSpec.command);
1015
+ if (completedQuota) {
1016
+ return yield* Effect.fail(completedQuota);
1017
+ }
1018
+ return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", completedError));
953
1019
  }
954
1020
  // Some CLIs may print extra banners to stdout. Allow individual agents
955
1021
  // to provide patterns so this logic stays opt-in and agent-specific.
@@ -1069,6 +1135,128 @@ export class BaseCliAgent {
1069
1135
  }
1070
1136
  /**
1071
1137
  * @param {AgentGenerateOptions} [options]
1138
+ * @returns {Promise<void>}
1139
+ */
1140
+ async preflight(options) {
1141
+ const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
1142
+ const env = {
1143
+ ...process.env,
1144
+ ...this.env,
1145
+ ...taskContextEnv(options?.taskContext),
1146
+ };
1147
+ const { systemFromMessages } = extractPrompt(options);
1148
+ const combinedSystem = combineNonEmpty([
1149
+ this.systemPrompt,
1150
+ systemFromMessages,
1151
+ ]);
1152
+ const agentId = this.id ?? "<anonymous>";
1153
+ const agentModel = this.model ?? "<unset>";
1154
+ let cleanup;
1155
+ let command = resolveAgentEngineTag(this);
1156
+ try {
1157
+ const commandSpec = await this.buildCommand({
1158
+ prompt: "",
1159
+ systemPrompt: combinedSystem,
1160
+ cwd,
1161
+ options,
1162
+ });
1163
+ cleanup = commandSpec.cleanup;
1164
+ command = commandSpec.command;
1165
+ const commandEnv = commandSpec.env
1166
+ ? { ...env, ...commandSpec.env }
1167
+ : env;
1168
+ const report = await launchDiagnostics(commandSpec.command, commandEnv, cwd, this.diagnosticHints?.());
1169
+ if (!report) {
1170
+ logDebug("agent preflight skipped; no diagnostics strategy", {
1171
+ agentId,
1172
+ agentEngine: commandSpec.command,
1173
+ agentModel,
1174
+ cwd,
1175
+ }, "agent.preflight");
1176
+ return;
1177
+ }
1178
+ const failed = report.checks.filter((check) => check.status === "fail");
1179
+ const errored = report.checks.filter((check) => check.status === "error");
1180
+ if (failed.length > 0) {
1181
+ const summary = formatDiagnosticSummary(report);
1182
+ logWarning(summary, {
1183
+ agentId,
1184
+ agentEngine: commandSpec.command,
1185
+ agentModel,
1186
+ cwd,
1187
+ }, "agent.preflight");
1188
+ throw new SmithersError("AGENT_CONFIG_INVALID", `Agent "${agentId}" (${commandSpec.command}, model=${agentModel}) failed preflight: ${summary}`, {
1189
+ failureRetryable: false,
1190
+ preflight: true,
1191
+ agentId,
1192
+ agentEngine: commandSpec.command,
1193
+ agentModel,
1194
+ command: commandSpec.command,
1195
+ diagnostics: report,
1196
+ });
1197
+ }
1198
+ if (errored.length > 0) {
1199
+ logWarning(`Agent preflight diagnostics had non-blocking errors: ${formatDiagnosticSummary(report)}`, {
1200
+ agentId,
1201
+ agentEngine: commandSpec.command,
1202
+ agentModel,
1203
+ cwd,
1204
+ }, "agent.preflight");
1205
+ }
1206
+ else {
1207
+ logDebug("agent preflight passed", {
1208
+ agentId,
1209
+ agentEngine: commandSpec.command,
1210
+ agentModel,
1211
+ cwd,
1212
+ durationMs: Math.round(report.durationMs),
1213
+ }, "agent.preflight");
1214
+ }
1215
+ }
1216
+ catch (cause) {
1217
+ if (cause instanceof SmithersError && cause.details?.preflight === true) {
1218
+ throw cause;
1219
+ }
1220
+ const normalized = cause instanceof SmithersError
1221
+ ? cause
1222
+ : toSmithersError(cause, "build agent preflight command", {
1223
+ code: "AGENT_CONFIG_INVALID",
1224
+ details: {
1225
+ failureRetryable: false,
1226
+ preflight: true,
1227
+ agentId,
1228
+ agentEngine: command,
1229
+ agentModel,
1230
+ command,
1231
+ },
1232
+ });
1233
+ throw new SmithersError(normalized.code ?? "AGENT_CONFIG_INVALID", `Agent "${agentId}" (${command}, model=${agentModel}) failed preflight: ${normalized.summary ?? normalized.message}`, {
1234
+ ...normalized.details,
1235
+ failureRetryable: false,
1236
+ preflight: true,
1237
+ agentId,
1238
+ agentEngine: normalized.details?.agentEngine ?? command,
1239
+ agentModel,
1240
+ command: normalized.details?.command ?? command,
1241
+ }, { cause: normalized });
1242
+ }
1243
+ finally {
1244
+ if (cleanup) {
1245
+ try {
1246
+ await cleanup();
1247
+ }
1248
+ catch (error) {
1249
+ logWarning("agent preflight cleanup failed", {
1250
+ agentId,
1251
+ agentEngine: command,
1252
+ error: error instanceof Error ? error.message : String(error),
1253
+ }, "agent.preflight");
1254
+ }
1255
+ }
1256
+ }
1257
+ }
1258
+ /**
1259
+ * @param {AgentGenerateOptions} [options]
1072
1260
  * @returns {Promise<GenerateTextResult<Record<string, never>, unknown>>}
1073
1261
  */
1074
1262
  async generate(options) {
@@ -24,7 +24,7 @@ function extractLastAssistantMessage(messages) {
24
24
  }
25
25
  /**
26
26
  * @param {unknown} parsed
27
- * @param {{ sawDeltaSinceBoundary: boolean }} state
27
+ * @param {{ sawDeltaSinceBoundary: boolean; lastFinalText?: string }} state
28
28
  * @returns {string[]}
29
29
  */
30
30
  function extractCliStreamTextChunks(parsed, state) {
@@ -45,6 +45,24 @@ function extractCliStreamTextChunks(parsed, state) {
45
45
  const emitFinal = (text) => {
46
46
  if (text && !state.sawDeltaSinceBoundary) {
47
47
  chunks.push(text);
48
+ state.lastFinalText = text;
49
+ }
50
+ state.sawDeltaSinceBoundary = false;
51
+ };
52
+ /**
53
+ * The end-of-run `result` payload echoes the final assistant message — e.g.
54
+ * Claude Code emits a complete `assistant` line AND a `result` line that both
55
+ * carry the same answer, with no deltas in between. Treat `result` as a
56
+ * fallback: surface it only when nothing already covered this turn and it is
57
+ * not a verbatim repeat of the text we just emitted. Without this the answer
58
+ * is streamed (and persisted as a NodeOutput) twice, so every consumer — the
59
+ * TUI, `smithers chat`, the gateway UI — shows it doubled.
60
+ * @param {string | undefined} text
61
+ */
62
+ const emitResult = (text) => {
63
+ if (text && !state.sawDeltaSinceBoundary && text !== state.lastFinalText) {
64
+ chunks.push(text);
65
+ state.lastFinalText = text;
48
66
  }
49
67
  state.sawDeltaSinceBoundary = false;
50
68
  };
@@ -103,7 +121,7 @@ function extractCliStreamTextChunks(parsed, state) {
103
121
  emitFinal(extractTextFromJsonValue(message));
104
122
  }
105
123
  if (type === "result") {
106
- emitFinal(extractTextFromJsonValue(record.result ?? record.response ?? record.output ?? record));
124
+ emitResult(extractTextFromJsonValue(record.result ?? record.response ?? record.output ?? record));
107
125
  }
108
126
  if (type === "turn_end" && message?.role === "assistant") {
109
127
  emitFinal(extractTextFromJsonValue(message));
@@ -132,7 +150,7 @@ export function createAgentStdoutTextEmitter(options) {
132
150
  const { outputFormat, onText } = options;
133
151
  let buffer = "";
134
152
  let emittedAnyText = false;
135
- const state = { sawDeltaSinceBoundary: false };
153
+ const state = { sawDeltaSinceBoundary: false, lastFinalText: undefined };
136
154
  /**
137
155
  * @param {string | undefined} text
138
156
  */