@trailstep/core 0.1.0 → 0.1.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.
package/dist/index.js CHANGED
@@ -192,17 +192,21 @@ function parseTargetArray(path, value, diagnostics) {
192
192
  `${targetPath}.thinking must be one of low, medium, high, xhigh, or max when present.`
193
193
  );
194
194
  }
195
+ const model = typeof target.model === "string" ? optionalNonEmptyString(target.model) : void 0;
195
196
  const args = parseOptionalStringArray(`${targetPath}.args`, target.args, diagnostics);
196
197
  return [
197
198
  {
198
199
  provider: target.provider,
199
- ...typeof target.model === "string" ? { model: target.model } : {},
200
+ ...model === void 0 ? {} : { model },
200
201
  ...typeof target.thinking === "string" && THINKING_LEVELS.has(target.thinking) ? { thinking: target.thinking } : {},
201
202
  ...args === void 0 ? {} : { args }
202
203
  }
203
204
  ];
204
205
  });
205
206
  }
207
+ function optionalNonEmptyString(value) {
208
+ return value.trim().length === 0 ? void 0 : value;
209
+ }
206
210
 
207
211
  // src/agent-targeting/parse-trailstep-config/parse-agent-mappings.ts
208
212
  function parseAgentMappings(path, value, diagnostics) {
@@ -218,6 +222,7 @@ function parseAgentMappings(path, value, diagnostics) {
218
222
  }
219
223
 
220
224
  // src/agent-targeting/parse-trailstep-config/parse-custom-providers.ts
225
+ var THINKING_LEVELS2 = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
221
226
  function parseCustomProviders(value, diagnostics) {
222
227
  const customProviders = {};
223
228
  if (!isRecord(value)) {
@@ -243,6 +248,16 @@ function parseCustomProviders(value, diagnostics) {
243
248
  providerConfig.interactiveArgs,
244
249
  diagnostics
245
250
  );
251
+ const model = parseCustomProviderModelSupport(
252
+ `customProviders.${name}.model`,
253
+ providerConfig.model,
254
+ diagnostics
255
+ );
256
+ const thinking = parseCustomProviderThinkingSupport(
257
+ `customProviders.${name}.thinking`,
258
+ providerConfig.thinking,
259
+ diagnostics
260
+ );
246
261
  const env = parseOptionalStringRecord(
247
262
  `customProviders.${name}.env`,
248
263
  providerConfig.env,
@@ -255,12 +270,74 @@ function parseCustomProviders(value, diagnostics) {
255
270
  binary: providerConfig.binary,
256
271
  ...args === void 0 ? {} : { args },
257
272
  ...interactiveArgs === void 0 ? {} : { interactiveArgs },
273
+ ...model === void 0 ? {} : { model },
274
+ ...thinking === void 0 ? {} : { thinking },
258
275
  ...typeof providerConfig.cwd === "string" ? { cwd: providerConfig.cwd } : {},
259
276
  ...env === void 0 ? {} : { env }
260
277
  };
261
278
  }
262
279
  return customProviders;
263
280
  }
281
+ function parseCustomProviderModelSupport(path, value, diagnostics) {
282
+ if (value === void 0) {
283
+ return void 0;
284
+ }
285
+ if (!isRecord(value) || typeof value.supported !== "boolean") {
286
+ diagnostics.push(`${path} must be an object with a boolean supported field when present.`);
287
+ return void 0;
288
+ }
289
+ if (!value.supported) {
290
+ return { supported: false };
291
+ }
292
+ if (value.flag !== void 0 && (typeof value.flag !== "string" || value.flag.length === 0)) {
293
+ diagnostics.push(`${path}.flag must be a non-empty string when present.`);
294
+ return void 0;
295
+ }
296
+ return {
297
+ supported: true,
298
+ ...typeof value.flag === "string" ? { flag: value.flag } : {}
299
+ };
300
+ }
301
+ function parseCustomProviderThinkingSupport(path, value, diagnostics) {
302
+ if (value === void 0) {
303
+ return void 0;
304
+ }
305
+ if (!isRecord(value) || typeof value.supported !== "boolean") {
306
+ diagnostics.push(`${path} must be an object with a boolean supported field when present.`);
307
+ return void 0;
308
+ }
309
+ if (!value.supported) {
310
+ if (value.levels !== void 0 && (!Array.isArray(value.levels) || value.levels.length > 0)) {
311
+ diagnostics.push(`${path}.levels must be an empty array when thinking support is disabled.`);
312
+ return void 0;
313
+ }
314
+ return value.levels === void 0 ? { supported: false } : { supported: false, levels: [] };
315
+ }
316
+ if (value.flag !== void 0 && (typeof value.flag !== "string" || value.flag.length === 0)) {
317
+ diagnostics.push(`${path}.flag must be a non-empty string when present.`);
318
+ return void 0;
319
+ }
320
+ if (!Array.isArray(value.levels) || value.levels.length === 0) {
321
+ diagnostics.push(`${path}.levels must be a non-empty array of supported thinking levels.`);
322
+ return void 0;
323
+ }
324
+ const levels = [];
325
+ for (const [index, level] of value.levels.entries()) {
326
+ if (typeof level !== "string" || !THINKING_LEVELS2.has(level)) {
327
+ diagnostics.push(`${path}.levels[${index}] must be one of low, medium, high, xhigh, or max.`);
328
+ continue;
329
+ }
330
+ levels.push(level);
331
+ }
332
+ if (levels.length !== value.levels.length) {
333
+ return void 0;
334
+ }
335
+ return {
336
+ supported: true,
337
+ ...typeof value.flag === "string" ? { flag: value.flag } : {},
338
+ levels
339
+ };
340
+ }
264
341
 
265
342
  // src/agent-targeting/parse-trailstep-config/parse-settings.ts
266
343
  function parseSettings(path, value, diagnostics) {
@@ -530,6 +607,41 @@ function promptFileReference(promptFile) {
530
607
 
531
608
  // src/known-cli-providers/providers/claude/claude-provider.ts
532
609
  var CLAUDE_BINARY = "claude";
610
+ var CLAUDE_RESULT_FIELD = "result";
611
+ var CLAUDE_SPEC = {
612
+ id: "claude",
613
+ displayName: "Claude",
614
+ model: { supported: true, flag: "--model" },
615
+ thinking: {
616
+ supported: true,
617
+ flag: "--effort",
618
+ levels: ["low", "medium", "high", "xhigh", "max"]
619
+ },
620
+ working: {
621
+ command: CLAUDE_BINARY,
622
+ prompt: { kind: "prompt-file", reference: "at-prefixed-argument" },
623
+ baseArgs: [
624
+ "-p",
625
+ "@{{promptFile}}",
626
+ "--output-format",
627
+ "json",
628
+ "--dangerously-skip-permissions"
629
+ ],
630
+ output: {
631
+ style: "stdout-json-envelope",
632
+ parsing: { resultField: CLAUDE_RESULT_FIELD }
633
+ },
634
+ repair: { supported: true, resumeFlag: "--resume", promptDelivery: "stdin" }
635
+ },
636
+ interactive: {
637
+ supported: true,
638
+ command: CLAUDE_BINARY,
639
+ requiresSystemPromptFile: true,
640
+ systemPromptFileFlag: "--append-system-prompt-file",
641
+ modelFlag: "--model",
642
+ permissionBypassFlag: "--dangerously-skip-permissions"
643
+ }
644
+ };
533
645
  async function runWorking(request, runner = spawnClaudeCapturingStdout) {
534
646
  const args = buildClaudeWorkingArgs(request);
535
647
  let result;
@@ -569,7 +681,7 @@ function extractFailureContext(rawStdout) {
569
681
  const metadata = extractEnvelopeMetadata(rawStdout, { harnessDurationMs: 0 });
570
682
  let rawResultText;
571
683
  try {
572
- rawResultText = extractEnvelopeText(rawStdout, { resultField: "result" });
684
+ rawResultText = extractEnvelopeText(rawStdout, { resultField: CLAUDE_RESULT_FIELD });
573
685
  } catch {
574
686
  rawResultText = void 0;
575
687
  }
@@ -583,7 +695,7 @@ async function writeCapturedOutput(options) {
583
695
  if (options.captureMode === "raw-text") {
584
696
  let text;
585
697
  try {
586
- text = extractEnvelopeText(options.stdout, { resultField: "result" });
698
+ text = extractEnvelopeText(options.stdout, { resultField: CLAUDE_RESULT_FIELD });
587
699
  } catch (error) {
588
700
  throw new TrailStepFailureError({
589
701
  code: "agent_provider_output_invalid",
@@ -598,7 +710,7 @@ async function writeCapturedOutput(options) {
598
710
  } else {
599
711
  let output;
600
712
  try {
601
- output = extractEnvelopeOutput(options.stdout, { resultField: "result" });
713
+ output = extractEnvelopeOutput(options.stdout, { resultField: CLAUDE_RESULT_FIELD });
602
714
  } catch (error) {
603
715
  throw new TrailStepFailureError({
604
716
  code: "agent_provider_output_invalid",
@@ -819,6 +931,7 @@ function terminateChildProcessTree(child) {
819
931
  }
820
932
  var claudeProvider = {
821
933
  id: "claude",
934
+ spec: CLAUDE_SPEC,
822
935
  runWorking,
823
936
  repairOutput,
824
937
  runInteractive
@@ -827,7 +940,31 @@ var claudeProvider = {
827
940
  // src/known-cli-providers/providers/codex/codex-provider.ts
828
941
  import { spawn as spawn2 } from "child_process";
829
942
  var CODEX_BINARY = "codex";
830
- var SUPPORTED_CODEX_THINKING = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh"]);
943
+ var CODEX_THINKING_LEVELS = ["low", "medium", "high", "xhigh"];
944
+ var SUPPORTED_CODEX_THINKING = new Set(CODEX_THINKING_LEVELS);
945
+ var CODEX_SPEC = {
946
+ id: "codex",
947
+ displayName: "Codex",
948
+ model: { supported: true, flag: "-m" },
949
+ thinking: {
950
+ supported: true,
951
+ flag: "-c model_reasoning_effort",
952
+ levels: CODEX_THINKING_LEVELS
953
+ },
954
+ working: {
955
+ command: CODEX_BINARY,
956
+ prompt: { kind: "prompt-file", reference: "at-prefixed-argument" },
957
+ baseArgs: [
958
+ "exec",
959
+ "--dangerously-bypass-approvals-and-sandbox",
960
+ "-o",
961
+ "{{outputFile}}",
962
+ "@{{promptFile}}"
963
+ ],
964
+ output: { style: "provider-output-file" }
965
+ },
966
+ interactive: { supported: true, command: CODEX_BINARY, modelFlag: "--model" }
967
+ };
831
968
  async function runWorking2(request, runner = spawnCodexInheritingStdio) {
832
969
  const args = buildCodexWorkingArgs(request);
833
970
  let result;
@@ -945,6 +1082,7 @@ function terminateChildProcessTree2(child) {
945
1082
  }
946
1083
  var codexProvider = {
947
1084
  id: "codex",
1085
+ spec: CODEX_SPEC,
948
1086
  runWorking: runWorking2,
949
1087
  runInteractive: runInteractive2
950
1088
  };
@@ -954,6 +1092,22 @@ import { spawn as spawn3 } from "child_process";
954
1092
  import { writeFile as writeFile2 } from "fs/promises";
955
1093
  var GEMINI_BINARY = "gemini";
956
1094
  var GEMINI_RESULT_FIELD = "response";
1095
+ var GEMINI_SPEC = {
1096
+ id: "gemini",
1097
+ displayName: "Gemini",
1098
+ model: { supported: true, flag: "-m" },
1099
+ thinking: { supported: false },
1100
+ working: {
1101
+ command: GEMINI_BINARY,
1102
+ prompt: { kind: "prompt-file", reference: "at-prefixed-argument" },
1103
+ baseArgs: ["-p", "@{{promptFile}}", "--yolo", "--output-format", "json"],
1104
+ output: {
1105
+ style: "stdout-json-envelope",
1106
+ parsing: { resultField: GEMINI_RESULT_FIELD }
1107
+ }
1108
+ },
1109
+ interactive: { supported: true, command: GEMINI_BINARY, modelFlag: "-m" }
1110
+ };
957
1111
  async function runWorking3(request, runner = spawnGeminiCapturingStdout) {
958
1112
  const args = buildGeminiWorkingArgs(request);
959
1113
  let result;
@@ -1095,6 +1249,7 @@ function terminateChildProcessTree3(child) {
1095
1249
  }
1096
1250
  var geminiProvider = {
1097
1251
  id: "gemini",
1252
+ spec: GEMINI_SPEC,
1098
1253
  runWorking: runWorking3,
1099
1254
  runInteractive: runInteractive3
1100
1255
  };
@@ -1203,6 +1358,34 @@ function windowsPathDelimiter() {
1203
1358
  var PI_BINARY = "pi";
1204
1359
  var PI_RESULT_FIELD = "message";
1205
1360
  var PI_STDOUT_FALLBACK_TAIL_CHARS = 128e3;
1361
+ var PI_SPEC = {
1362
+ id: "pi",
1363
+ displayName: "Pi",
1364
+ model: {
1365
+ supported: true,
1366
+ flag: "--model",
1367
+ discovery: {
1368
+ command: PI_BINARY,
1369
+ args: ["--list-models"],
1370
+ outputParser: "pi-list-models-table"
1371
+ }
1372
+ },
1373
+ thinking: {
1374
+ supported: true,
1375
+ flag: "--thinking",
1376
+ levels: ["low", "medium", "high", "xhigh", "max"]
1377
+ },
1378
+ working: {
1379
+ command: PI_BINARY,
1380
+ prompt: { kind: "prompt-file", reference: "at-prefixed-argument" },
1381
+ baseArgs: ["-p", "@{{promptFile}}", "--mode", "json"],
1382
+ output: {
1383
+ style: "stdout-jsonl-transcript",
1384
+ parsing: { resultField: PI_RESULT_FIELD }
1385
+ }
1386
+ },
1387
+ interactive: { supported: true, command: PI_BINARY, modelFlag: "--model" }
1388
+ };
1206
1389
  async function runWorking4(request, runner = spawnPiCapturingStdout) {
1207
1390
  const args = buildPiWorkingArgs(request);
1208
1391
  let result;
@@ -1386,6 +1569,7 @@ function terminateChildProcessTree4(child) {
1386
1569
  }
1387
1570
  var piProvider = {
1388
1571
  id: "pi",
1572
+ spec: PI_SPEC,
1389
1573
  runWorking: runWorking4,
1390
1574
  runInteractive: runInteractive4
1391
1575
  };
@@ -1511,6 +1695,7 @@ function defaultRunsRoot(cwd) {
1511
1695
  }
1512
1696
  async function createRunDirectory(options) {
1513
1697
  const runsRoot = options.runsRoot ?? defaultRunsRoot(options.cwd);
1698
+ validateRunName(options.runName);
1514
1699
  await mkdir(runsRoot, { recursive: true });
1515
1700
  await ensureTrailStepGitignoreForRunsRoot(runsRoot);
1516
1701
  for (let suffix = 1; ; suffix += 1) {
@@ -1593,6 +1778,15 @@ async function writeRunState(runDir, state2) {
1593
1778
  function isNodeError(error) {
1594
1779
  return error instanceof Error && "code" in error;
1595
1780
  }
1781
+ function validateRunName(runName) {
1782
+ if (runName.trim().length === 0 || runName === "." || runName === ".." || runName.includes("/") || runName.includes("\\")) {
1783
+ throw new TrailStepFailureError({
1784
+ code: "run_name_invalid",
1785
+ message: "Run name must be a non-empty single path segment.",
1786
+ details: { runName }
1787
+ });
1788
+ }
1789
+ }
1596
1790
  function isRunState(value) {
1597
1791
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
1598
1792
  }
@@ -2113,6 +2307,168 @@ function formatDiagnostics3(diagnostics) {
2113
2307
  return diagnostics.map((diagnostic) => `${diagnostic.path} ${diagnostic.message}`).join("; ");
2114
2308
  }
2115
2309
 
2310
+ // src/agent-execution/custom-provider/render-custom-provider-args.ts
2311
+ var PLACEHOLDER_NAMES = /* @__PURE__ */ new Set([
2312
+ "prompt",
2313
+ "promptFile",
2314
+ "outputFile",
2315
+ "model",
2316
+ "thinking"
2317
+ ]);
2318
+ var CONDITIONAL_NAMES = /* @__PURE__ */ new Set(["model", "thinking"]);
2319
+ var TEMPLATE_TOKEN_PATTERN = /^{{([#/]?)([A-Za-z][A-Za-z0-9]*)}}$/;
2320
+ function renderCustomProviderArgs(options) {
2321
+ const values = {
2322
+ ...options.values.prompt === void 0 ? {} : { prompt: options.values.prompt },
2323
+ ...options.values.promptFile === void 0 ? {} : { promptFile: options.values.promptFile },
2324
+ ...options.values.outputFile === void 0 ? {} : { outputFile: options.values.outputFile },
2325
+ ...optionalNonEmptyString2(options.values.model) === void 0 ? {} : { model: optionalNonEmptyString2(options.values.model) },
2326
+ ...options.values.thinking === void 0 ? {} : { thinking: options.values.thinking }
2327
+ };
2328
+ return renderRange({
2329
+ argv: options.argv,
2330
+ values,
2331
+ errorCode: options.errorCode,
2332
+ commandDescription: options.commandDescription
2333
+ });
2334
+ }
2335
+ function renderRange(options) {
2336
+ const rendered = [];
2337
+ for (let index = 0; index < options.argv.length; index += 1) {
2338
+ const arg = options.argv[index] ?? "";
2339
+ const token = parseTemplateToken(arg);
2340
+ if (token?.kind === "open") {
2341
+ const closeIndex = findConditionalClose({
2342
+ argv: options.argv,
2343
+ openIndex: index,
2344
+ condition: token.name,
2345
+ errorCode: options.errorCode,
2346
+ commandDescription: options.commandDescription
2347
+ });
2348
+ if (options.values[token.name] !== void 0) {
2349
+ rendered.push(
2350
+ ...renderRange({
2351
+ ...options,
2352
+ argv: options.argv.slice(index + 1, closeIndex)
2353
+ })
2354
+ );
2355
+ }
2356
+ index = closeIndex;
2357
+ continue;
2358
+ }
2359
+ if (token?.kind === "close") {
2360
+ throw templateError({
2361
+ code: options.errorCode,
2362
+ message: `${options.commandDescription} template has unexpected ${closingToken(token.name)} without a matching ${openingToken(token.name)}.`
2363
+ });
2364
+ }
2365
+ if (token?.kind === "placeholder") {
2366
+ rendered.push(renderPlaceholder({ ...options, name: token.name }));
2367
+ continue;
2368
+ }
2369
+ if (TEMPLATE_TOKEN_PATTERN.test(arg)) {
2370
+ throw templateError({
2371
+ code: options.errorCode,
2372
+ message: `${options.commandDescription} template token ${arg} is not supported.`,
2373
+ details: { arg }
2374
+ });
2375
+ }
2376
+ if (arg.includes("{{") || arg.includes("}}")) {
2377
+ throw templateError({
2378
+ code: options.errorCode,
2379
+ message: `${options.commandDescription} placeholders and conditional blocks must be whole argv values.`,
2380
+ details: { arg }
2381
+ });
2382
+ }
2383
+ rendered.push(arg);
2384
+ }
2385
+ return rendered;
2386
+ }
2387
+ function parseTemplateToken(arg) {
2388
+ const match = TEMPLATE_TOKEN_PATTERN.exec(arg);
2389
+ if (!match) {
2390
+ return void 0;
2391
+ }
2392
+ const prefix = match[1] ?? "";
2393
+ const name = match[2] ?? "";
2394
+ if (prefix === "") {
2395
+ if (PLACEHOLDER_NAMES.has(name)) {
2396
+ return { kind: "placeholder", name };
2397
+ }
2398
+ return void 0;
2399
+ }
2400
+ if (CONDITIONAL_NAMES.has(name)) {
2401
+ return {
2402
+ kind: prefix === "#" ? "open" : "close",
2403
+ name
2404
+ };
2405
+ }
2406
+ return void 0;
2407
+ }
2408
+ function findConditionalClose(options) {
2409
+ let depth = 1;
2410
+ for (let index = options.openIndex + 1; index < options.argv.length; index += 1) {
2411
+ const token = parseTemplateToken(options.argv[index] ?? "");
2412
+ if (token?.name !== options.condition) {
2413
+ continue;
2414
+ }
2415
+ if (token.kind === "open") {
2416
+ depth += 1;
2417
+ continue;
2418
+ }
2419
+ if (token.kind === "close") {
2420
+ depth -= 1;
2421
+ if (depth === 0) {
2422
+ return index;
2423
+ }
2424
+ }
2425
+ }
2426
+ throw templateError({
2427
+ code: options.errorCode,
2428
+ message: `${options.commandDescription} template is missing ${closingToken(options.condition)} for ${openingToken(options.condition)}.`
2429
+ });
2430
+ }
2431
+ function renderPlaceholder(options) {
2432
+ const value = options.values[options.name];
2433
+ if (value !== void 0) {
2434
+ return value;
2435
+ }
2436
+ if (options.name === "model" || options.name === "thinking") {
2437
+ throw templateError({
2438
+ code: options.errorCode,
2439
+ message: `${options.commandDescription} placeholder ${placeholderToken(options.name)} must be guarded with ${openingToken(options.name)} ... ${closingToken(options.name)} when no ${options.name} override is set.`,
2440
+ details: { placeholder: placeholderToken(options.name) }
2441
+ });
2442
+ }
2443
+ throw templateError({
2444
+ code: options.errorCode,
2445
+ message: `${options.commandDescription} placeholder ${placeholderToken(options.name)} is not available in this command template.`,
2446
+ details: { placeholder: placeholderToken(options.name) }
2447
+ });
2448
+ }
2449
+ function templateError(options) {
2450
+ return new TrailStepFailureError({
2451
+ code: options.code,
2452
+ message: options.message,
2453
+ ...options.details === void 0 ? {} : { details: options.details }
2454
+ });
2455
+ }
2456
+ function placeholderToken(name) {
2457
+ return `{{${name}}}`;
2458
+ }
2459
+ function openingToken(name) {
2460
+ return `{{#${name}}}`;
2461
+ }
2462
+ function closingToken(name) {
2463
+ return `{{/${name}}}`;
2464
+ }
2465
+ function optionalNonEmptyString2(value) {
2466
+ if (value === void 0 || value.trim().length === 0) {
2467
+ return void 0;
2468
+ }
2469
+ return value;
2470
+ }
2471
+
2116
2472
  // src/agent-execution/interactive-agent/run-interactive-agent-command.ts
2117
2473
  async function runInteractiveAgentCommand(options) {
2118
2474
  const [target] = resolveAgentTargets({
@@ -2187,12 +2543,22 @@ async function runInteractiveAgentTarget(options) {
2187
2543
  details: { provider: options.target.provider }
2188
2544
  });
2189
2545
  }
2190
- const args = await substitutePromptPlaceholders({
2546
+ const thinking = options.target.thinking ?? options.role.thinking;
2547
+ const args = renderCustomProviderArgs({
2191
2548
  argv: options.target.args ?? agentConfig.interactiveArgs,
2192
- prompt,
2193
- promptFile: files.promptFile,
2194
- model: options.target.model
2549
+ values: {
2550
+ prompt,
2551
+ promptFile: files.promptFile,
2552
+ ...options.target.model === void 0 ? {} : { model: options.target.model },
2553
+ ...thinking === void 0 ? {} : { thinking }
2554
+ },
2555
+ errorCode: "interactive_command_invalid",
2556
+ commandDescription: "Interactive agent command"
2195
2557
  });
2558
+ if (args.includes(files.promptFile)) {
2559
+ await mkdir2(dirname3(files.promptFile), { recursive: true });
2560
+ await writeFile5(files.promptFile, prompt, "utf8");
2561
+ }
2196
2562
  return await runProcessUntilExitOrCompletion({
2197
2563
  stepId: options.stepId,
2198
2564
  target: options.target.provider,
@@ -2331,33 +2697,6 @@ function exampleJsonObject(schema) {
2331
2697
  })
2332
2698
  );
2333
2699
  }
2334
- async function substitutePromptPlaceholders(options) {
2335
- let needsPromptFile = false;
2336
- const substituted = options.argv.map((arg) => {
2337
- if (arg === "{{prompt}}") {
2338
- return options.prompt;
2339
- }
2340
- if (arg === "{{promptFile}}") {
2341
- needsPromptFile = true;
2342
- return options.promptFile;
2343
- }
2344
- if (arg === "{{model}}") {
2345
- return options.model ?? "";
2346
- }
2347
- if (arg.includes("{{prompt}}") || arg.includes("{{promptFile}}") || arg.includes("{{model}}")) {
2348
- throw new TrailStepFailureError({
2349
- code: "interactive_command_invalid",
2350
- message: "Interactive prompt placeholders must be whole argv values."
2351
- });
2352
- }
2353
- return arg;
2354
- });
2355
- if (needsPromptFile) {
2356
- await mkdir2(dirname3(options.promptFile), { recursive: true });
2357
- await writeFile5(options.promptFile, options.prompt, "utf8");
2358
- }
2359
- return substituted;
2360
- }
2361
2700
  var spawnInteractiveProcess = async ({
2362
2701
  command,
2363
2702
  args,
@@ -2503,23 +2842,16 @@ function buildWorkingAgentArgs(options) {
2503
2842
  ...options.model ? ["--model", options.model] : []
2504
2843
  ];
2505
2844
  }
2506
- return options.argv.map((arg) => {
2507
- switch (arg) {
2508
- case "{{promptFile}}":
2509
- return options.promptFile;
2510
- case "{{outputFile}}":
2511
- return options.outputFile;
2512
- case "{{model}}":
2513
- return options.model ?? "";
2514
- default:
2515
- if (arg.includes("{{promptFile}}") || arg.includes("{{outputFile}}") || arg.includes("{{model}}")) {
2516
- throw new TrailStepFailureError({
2517
- code: "agent_provider_invalid",
2518
- message: "Working agent command placeholders must be whole argv values."
2519
- });
2520
- }
2521
- return arg;
2522
- }
2845
+ return renderCustomProviderArgs({
2846
+ argv: options.argv,
2847
+ values: {
2848
+ promptFile: options.promptFile,
2849
+ outputFile: options.outputFile,
2850
+ ...options.model === void 0 ? {} : { model: options.model },
2851
+ ...options.thinking === void 0 ? {} : { thinking: options.thinking }
2852
+ },
2853
+ errorCode: "agent_provider_invalid",
2854
+ commandDescription: "Working agent command"
2523
2855
  });
2524
2856
  }
2525
2857
 
@@ -2573,11 +2905,13 @@ async function runCustomWorkingProvider(options) {
2573
2905
  details: { provider: options.target.provider }
2574
2906
  });
2575
2907
  }
2908
+ const thinking = options.target.thinking ?? options.role.thinking;
2576
2909
  const args = buildWorkingAgentArgs({
2577
2910
  argv: options.target.args ?? agentConfig.args,
2578
2911
  promptFile: options.files.promptFile,
2579
2912
  outputFile: options.files.outputFile,
2580
- model: options.target.model
2913
+ model: options.target.model,
2914
+ thinking
2581
2915
  });
2582
2916
  let result;
2583
2917
  try {
@@ -2786,6 +3120,7 @@ async function runWorkingAgentTargetAttempt(options) {
2786
3120
  return runCustomWorkingProvider({
2787
3121
  config: options.config,
2788
3122
  step: options.step,
3123
+ role: options.role,
2789
3124
  cwd: options.cwd,
2790
3125
  runner: options.runner,
2791
3126
  target: options.target,
@@ -4315,7 +4650,13 @@ function isFlattenedAgentMappings(value) {
4315
4650
  );
4316
4651
  }
4317
4652
  function isFlattenedAgentTarget(value) {
4318
- return isPlainRecord(value) && typeof value.provider === "string";
4653
+ if (!isPlainRecord(value) || typeof value.provider !== "string") {
4654
+ return false;
4655
+ }
4656
+ if (value.model !== void 0) {
4657
+ return typeof value.model === "string" && value.model.trim().length > 0;
4658
+ }
4659
+ return true;
4319
4660
  }
4320
4661
  function isPlainRecord(value) {
4321
4662
  return typeof value === "object" && value !== null && !Array.isArray(value);