@smithers-orchestrator/agents 0.24.0 → 0.24.2

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.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "AI SDK and CLI agent adapters for Smithers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -56,9 +56,9 @@
56
56
  "ai": "^6.0.168",
57
57
  "effect": "^3.21.1",
58
58
  "zod": "^4.3.6",
59
- "@smithers-orchestrator/driver": "0.24.0",
60
- "@smithers-orchestrator/errors": "0.24.0",
61
- "@smithers-orchestrator/observability": "0.24.0"
59
+ "@smithers-orchestrator/driver": "0.24.2",
60
+ "@smithers-orchestrator/errors": "0.24.2",
61
+ "@smithers-orchestrator/observability": "0.24.2"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/bun": "latest",
@@ -862,7 +862,7 @@ export class BaseCliAgent {
862
862
  }
863
863
  flushBufferedLines(stream, false);
864
864
  };
865
- diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd);
865
+ diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd, this.diagnosticHints?.());
866
866
  return Effect.gen(this, function* () {
867
867
  const result = yield* runCommandEffect(commandSpec.command, commandSpec.args, {
868
868
  cwd,
@@ -1088,4 +1088,10 @@ export class BaseCliAgent {
1088
1088
  createOutputInterpreter() {
1089
1089
  return undefined;
1090
1090
  }
1091
+ /**
1092
+ * @returns {{ provider?: string; model?: string } | undefined}
1093
+ */
1094
+ diagnosticHints() {
1095
+ return undefined;
1096
+ }
1091
1097
  }
package/src/CodexAgent.js CHANGED
@@ -59,6 +59,15 @@ export class CodexAgent extends BaseCliAgent {
59
59
  super(opts);
60
60
  this.opts = opts;
61
61
  this.capabilities = createCodexCapabilityRegistry(opts);
62
+ // Native structured output (`codex exec --output-schema`) constrains the
63
+ // model to emit only final JSON and makes it refuse tool calls ("tool calls
64
+ // are constrained by a JSON response schema"), which breaks any agentic task
65
+ // (read/edit/run). It is therefore OPT-IN: by default Codex is treated like
66
+ // the other CLI engines (supportsNativeStructuredOutput=false), so the engine
67
+ // prompt-injects the schema and extracts JSON from the agent's final text,
68
+ // leaving tool use intact. Set nativeStructuredOutput:true for pure, tool-free
69
+ // extraction tasks that want strict schema enforcement.
70
+ this.supportsNativeStructuredOutput = opts.nativeStructuredOutput === true;
62
71
  }
63
72
  /**
64
73
  * @returns {CliOutputInterpreter}
@@ -548,10 +557,12 @@ export class CodexAgent extends BaseCliAgent {
548
557
  // turn.completed with token usage for metrics. extractUsageFromOutput
549
558
  // in BaseCliAgent will parse these automatically.
550
559
  args.push("--json");
551
- // Auto-wire output schema from task context if not explicitly set.
560
+ // Auto-wire output schema from task context if not explicitly set — only when
561
+ // native structured output is opted in. Otherwise the engine handles the schema
562
+ // via prompt-injection and Codex keeps full tool access (see constructor note).
552
563
  // Skip when resuming — `codex exec resume` does not accept --output-schema.
553
564
  let schemaCleanupFile = null;
554
- if (!resumeSession && !this.opts.outputSchema && params.options?.outputSchema) {
565
+ if (!resumeSession && this.opts.nativeStructuredOutput === true && !this.opts.outputSchema && params.options?.outputSchema) {
555
566
  const schema = params.options.outputSchema;
556
567
  const { z } = await import("zod");
557
568
  let jsonSchema = z.toJSONSchema(schema);
@@ -17,6 +17,17 @@ export type CodexAgentOptions = BaseCliAgentOptions & {
17
17
  skipGitRepoCheck?: boolean;
18
18
  addDir?: string[];
19
19
  outputSchema?: string;
20
+ /**
21
+ * Opt in to Codex's native structured output (`codex exec --output-schema`).
22
+ *
23
+ * Defaults to `false`. Native structured output makes the model emit only the
24
+ * final JSON and refuse tool calls, so it BREAKS agentic tasks (read/edit/run) —
25
+ * Codex returns `blocked` with no changes. Left off, Smithers treats Codex like
26
+ * the other CLI engines: it prompt-injects the schema and extracts JSON from the
27
+ * agent's final message, so tool use stays intact. Enable only for pure, tool-free
28
+ * extraction tasks that need strict schema enforcement.
29
+ */
30
+ nativeStructuredOutput?: boolean;
20
31
  color?: "always" | "never" | "auto";
21
32
  json?: boolean;
22
33
  outputLastMessage?: string;
package/src/PiAgent.js CHANGED
@@ -104,11 +104,14 @@ export class PiAgent extends BaseCliAgent {
104
104
  : undefined;
105
105
  const effectiveSession = resumeSession ?? this.opts.session;
106
106
  this.issuedSessionRef = effectiveSession;
107
- if (params.mode === "text") {
108
- if (this.opts.print !== false)
109
- args.push("--print");
107
+ // pi's --print (non-interactive: process prompt and exit) and --mode
108
+ // are independent. Apply --print to every non-RPC mode so json task
109
+ // executions also process one prompt and exit instead of lingering as
110
+ // an interactive session (#284).
111
+ if (params.mode !== "rpc" && this.opts.print !== false) {
112
+ args.push("--print");
110
113
  }
111
- else {
114
+ if (params.mode !== "text") {
112
115
  args.push("--mode", params.mode);
113
116
  }
114
117
  pushFlag(args, "--provider", this.opts.provider);
@@ -197,7 +200,9 @@ export class PiAgent extends BaseCliAgent {
197
200
  createOutputInterpreter() {
198
201
  let sessionId = this.issuedSessionRef;
199
202
  let emittedStarted = false;
203
+ let emittedCompleted = false;
200
204
  let finalAnswer = "";
205
+ let finalUsage;
201
206
  /**
202
207
  * @param {unknown} value
203
208
  */
@@ -231,6 +236,17 @@ export class PiAgent extends BaseCliAgent {
231
236
  }];
232
237
  };
233
238
  /**
239
+ * @param {Record<string, unknown>} payload
240
+ */
241
+ const captureUsage = (payload) => {
242
+ const candidate = (payload && typeof payload.usage === "object" && payload.usage) ||
243
+ (payload && typeof payload.message === "object" && payload.message && typeof payload.message.usage === "object" && payload.message.usage) ||
244
+ undefined;
245
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
246
+ finalUsage = candidate;
247
+ }
248
+ };
249
+ /**
234
250
  * @param {string} line
235
251
  * @returns {AgentCliEvent[]}
236
252
  */
@@ -267,6 +283,7 @@ export class PiAgent extends BaseCliAgent {
267
283
  return startedEvents();
268
284
  }
269
285
  if (type === "message_end" || type === "turn_end") {
286
+ captureUsage(payload);
270
287
  const message = payload.message;
271
288
  if (message?.role === "assistant") {
272
289
  const extracted = extractTextFromJsonValue(message);
@@ -276,6 +293,33 @@ export class PiAgent extends BaseCliAgent {
276
293
  }
277
294
  return startedEvents();
278
295
  }
296
+ if (type === "agent_end") {
297
+ captureUsage(payload);
298
+ if (Array.isArray(payload.messages)) {
299
+ for (let i = payload.messages.length - 1; i >= 0; i--) {
300
+ const message = payload.messages[i];
301
+ if (message?.role === "assistant") {
302
+ const extracted = extractTextFromJsonValue(message);
303
+ if (extracted) {
304
+ finalAnswer = extracted;
305
+ }
306
+ break;
307
+ }
308
+ }
309
+ }
310
+ emittedCompleted = true;
311
+ return [
312
+ ...startedEvents(),
313
+ {
314
+ type: "completed",
315
+ engine: this.cliEngine,
316
+ ok: true,
317
+ answer: finalAnswer || undefined,
318
+ usage: finalUsage,
319
+ resume: sessionId,
320
+ },
321
+ ];
322
+ }
279
323
  if (type === "tool_execution_start") {
280
324
  const toolName = asString(payload.toolName) ?? "tool";
281
325
  const toolId = asString(payload.toolCallId) ?? toolName;
@@ -355,6 +399,9 @@ export class PiAgent extends BaseCliAgent {
355
399
  const started = !emittedStarted && sessionId
356
400
  ? startedEvents()
357
401
  : [];
402
+ if (emittedCompleted) {
403
+ return started;
404
+ }
358
405
  return [
359
406
  ...started,
360
407
  {
@@ -362,6 +409,7 @@ export class PiAgent extends BaseCliAgent {
362
409
  engine: this.cliEngine,
363
410
  ok: !result.exitCode || result.exitCode === 0,
364
411
  answer: finalAnswer || undefined,
412
+ usage: finalUsage,
365
413
  error: result.exitCode && result.exitCode !== 0
366
414
  ? result.stderr.trim() || `PI exited with code ${result.exitCode}`
367
415
  : undefined,
@@ -394,7 +442,7 @@ export class PiAgent extends BaseCliAgent {
394
442
  const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
395
443
  const env = { ...process.env, ...this.env };
396
444
  const args = this.buildArgs({ prompt, cwd, options, mode });
397
- const diagnosticsPromise = launchDiagnostics("pi", env, cwd);
445
+ const diagnosticsPromise = launchDiagnostics("pi", env, cwd, this.diagnosticHints());
398
446
  const interpreter = this.createOutputInterpreter();
399
447
  /**
400
448
  * @param {AgentCliEvent[] | AgentCliEvent | null | undefined} payload
@@ -465,4 +513,14 @@ export class PiAgent extends BaseCliAgent {
465
513
  outputFormat: mode,
466
514
  };
467
515
  }
516
+ /**
517
+ * @returns {{ provider?: string; model?: string; apiKey?: string }}
518
+ */
519
+ diagnosticHints() {
520
+ return {
521
+ provider: this.opts.provider,
522
+ model: this.opts.model ?? this.model,
523
+ apiKey: this.opts.apiKey,
524
+ };
525
+ }
468
526
  }
@@ -9,6 +9,9 @@ import { spawnSync } from "node:child_process";
9
9
  /**
10
10
  * @typedef {{ id: DiagnosticCheckId; run: (ctx: DiagnosticContext) => Promise<DiagnosticCheck>; }} DiagnosticCheckDef
11
11
  */
12
+ /**
13
+ * @typedef {{ provider?: string; model?: string; apiKey?: string }} DiagnosticHints
14
+ */
12
15
 
13
16
  // ---------------------------------------------------------------------------
14
17
  // Shared check helpers
@@ -472,15 +475,75 @@ const antigravityStrategy = {
472
475
  // ---------------------------------------------------------------------------
473
476
  // Pi strategy
474
477
  // ---------------------------------------------------------------------------
475
- const piStrategy = {
476
- agentId: "pi",
477
- command: "pi",
478
- checks: [
479
- checkCliInstalled("pi", "Pi"),
480
- googleAuthCheck,
481
- googleRateLimitCheck,
482
- ],
483
- };
478
+ /**
479
+ * Resolve the effective pi provider family from an explicit `--provider`, a
480
+ * `provider/model` prefix, or a bare model id's well-known prefix. Returns ""
481
+ * when undeterminable so callers fall back to pi's default (google) (#284).
482
+ * @param {DiagnosticHints | undefined} hints
483
+ * @returns {string}
484
+ */
485
+ function resolvePiProvider(hints) {
486
+ const explicit = (hints?.provider || "").trim().toLowerCase();
487
+ if (explicit) {
488
+ return explicit;
489
+ }
490
+ const model = typeof hints?.model === "string" ? hints.model.trim().toLowerCase() : "";
491
+ if (!model) {
492
+ return "";
493
+ }
494
+ if (model.includes("/")) {
495
+ return model.split("/")[0];
496
+ }
497
+ // Bare model id (no provider prefix) — infer the provider family from
498
+ // common id prefixes so diagnostics probe the right backend.
499
+ if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o3-") || model.startsWith("o4-") || model.startsWith("chatgpt")) {
500
+ return "openai";
501
+ }
502
+ if (model.startsWith("claude")) {
503
+ return "anthropic";
504
+ }
505
+ if (model.startsWith("gemini")) {
506
+ return "google";
507
+ }
508
+ return "";
509
+ }
510
+ /**
511
+ * @param {DiagnosticHints | undefined} hints
512
+ * @returns {DiagnosticCheckDef[]}
513
+ */
514
+ function piProviderChecks(hints) {
515
+ const raw = resolvePiProvider(hints);
516
+ if (raw === "openai" || raw === "openai-codex" || raw === "azure" || raw === "azure-openai") {
517
+ return [...codexApiKeyAndRateLimitCheck];
518
+ }
519
+ if (raw === "anthropic" || raw === "claude") {
520
+ return [claudeApiKeyCheck, claudeRateLimitCheck];
521
+ }
522
+ return [googleAuthCheck, googleRateLimitCheck];
523
+ }
524
+ /**
525
+ * pi accepts credentials via the `--api-key` option instead of an environment
526
+ * variable. Diagnostics only see the process env, so map an explicit apiKey to
527
+ * the env var the selected provider's checks read — otherwise an apiKey-only pi
528
+ * run is misreported as "key missing" (#284). Returns undefined when there is
529
+ * nothing to inject.
530
+ * @param {string} command
531
+ * @param {DiagnosticHints | undefined} hints
532
+ * @returns {Record<string, string> | undefined}
533
+ */
534
+ export function diagnosticApiKeyEnv(command, hints) {
535
+ if (command !== "pi" || !hints?.apiKey) {
536
+ return undefined;
537
+ }
538
+ const raw = resolvePiProvider(hints);
539
+ if (raw === "openai" || raw === "openai-codex" || raw === "azure" || raw === "azure-openai") {
540
+ return { OPENAI_API_KEY: hints.apiKey };
541
+ }
542
+ if (raw === "anthropic" || raw === "claude") {
543
+ return { ANTHROPIC_API_KEY: hints.apiKey };
544
+ }
545
+ return { GOOGLE_API_KEY: hints.apiKey };
546
+ }
484
547
  // ---------------------------------------------------------------------------
485
548
  // Amp strategy
486
549
  // ---------------------------------------------------------------------------
@@ -524,13 +587,20 @@ const strategies = {
524
587
  antigravity: antigravityStrategy,
525
588
  agy: antigravityStrategy,
526
589
  gemini: geminiStrategy,
527
- pi: piStrategy,
528
590
  amp: ampStrategy,
529
591
  };
530
592
  /**
531
593
  * @param {string} command
594
+ * @param {DiagnosticHints} [hints]
532
595
  * @returns {AgentDiagnosticStrategy | null}
533
596
  */
534
- export function getDiagnosticStrategy(command) {
597
+ export function getDiagnosticStrategy(command, hints) {
598
+ if (command === "pi") {
599
+ return {
600
+ agentId: "pi",
601
+ command: "pi",
602
+ checks: [checkCliInstalled("pi", "Pi"), ...piProviderChecks(hints)],
603
+ };
604
+ }
535
605
  return strategies[command] ?? null;
536
606
  }
@@ -1,4 +1,4 @@
1
- import { getDiagnosticStrategy } from "./getDiagnosticStrategy.js";
1
+ import { diagnosticApiKeyEnv, getDiagnosticStrategy } from "./getDiagnosticStrategy.js";
2
2
  import { runDiagnostics } from "./runDiagnostics.js";
3
3
  /** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
4
4
 
@@ -6,11 +6,14 @@ import { runDiagnostics } from "./runDiagnostics.js";
6
6
  * @param {string} command
7
7
  * @param {Record<string, string>} env
8
8
  * @param {string} cwd
9
+ * @param {{ provider?: string; model?: string; apiKey?: string }} [hints]
9
10
  * @returns {Promise<DiagnosticReport> | null}
10
11
  */
11
- export function launchDiagnostics(command, env, cwd) {
12
- const strategy = getDiagnosticStrategy(command);
12
+ export function launchDiagnostics(command, env, cwd, hints) {
13
+ const strategy = getDiagnosticStrategy(command, hints);
13
14
  if (!strategy)
14
15
  return null;
15
- return runDiagnostics(strategy, { env, cwd }).catch(() => null);
16
+ const apiKeyEnv = diagnosticApiKeyEnv(command, hints);
17
+ const effectiveEnv = apiKeyEnv ? { ...env, ...apiKeyEnv } : env;
18
+ return runDiagnostics(strategy, { env: effectiveEnv, cwd }).catch(() => null);
16
19
  }