@orchagent/cli 0.3.86 → 0.3.87

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.
@@ -37,10 +37,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.localCommandForEntrypoint = localCommandForEntrypoint;
40
+ exports.validateInputSchema = validateInputSchema;
40
41
  exports.isKeyedFileArg = isKeyedFileArg;
41
42
  exports.readKeyedFiles = readKeyedFiles;
42
43
  exports.mountDirectory = mountDirectory;
43
44
  exports.buildInjectedPayload = buildInjectedPayload;
45
+ exports.renderProgress = renderProgress;
44
46
  exports.registerRunCommand = registerRunCommand;
45
47
  const promises_1 = __importDefault(require("fs/promises"));
46
48
  const path_1 = __importDefault(require("path"));
@@ -188,6 +190,69 @@ function applySchemaDefaults(body, schema) {
188
190
  }
189
191
  return body;
190
192
  }
193
+ /**
194
+ * Validates input data against an agent's input_schema before sending to the server.
195
+ * Returns an array of human-readable error strings. Empty array = valid.
196
+ */
197
+ function validateInputSchema(body, schema) {
198
+ if (!schema)
199
+ return [];
200
+ const s = schema;
201
+ const props = s.properties;
202
+ if (!props || typeof props !== 'object')
203
+ return [];
204
+ const errors = [];
205
+ const required = (s.required ?? []);
206
+ // Check required fields
207
+ for (const field of required) {
208
+ if (body[field] === undefined || body[field] === null) {
209
+ const prop = props[field];
210
+ const desc = prop?.description ? ` (${prop.description})` : '';
211
+ errors.push(`Missing required field: "${field}"${desc}`);
212
+ }
213
+ }
214
+ // Check types of provided fields
215
+ for (const [key, value] of Object.entries(body)) {
216
+ const prop = props[key];
217
+ if (!prop || value === undefined || value === null)
218
+ continue;
219
+ const expectedType = prop.type;
220
+ if (!expectedType)
221
+ continue;
222
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
223
+ let mismatch = false;
224
+ switch (expectedType) {
225
+ case 'string':
226
+ mismatch = actualType !== 'string';
227
+ break;
228
+ case 'number':
229
+ case 'integer':
230
+ mismatch = actualType !== 'number';
231
+ break;
232
+ case 'boolean':
233
+ mismatch = actualType !== 'boolean';
234
+ break;
235
+ case 'array':
236
+ mismatch = actualType !== 'array';
237
+ break;
238
+ case 'object':
239
+ mismatch = actualType !== 'object';
240
+ break;
241
+ }
242
+ if (mismatch) {
243
+ errors.push(`Field "${key}" should be ${expectedType}, got ${actualType}`);
244
+ }
245
+ }
246
+ return errors;
247
+ }
248
+ function warnInputSchemaErrors(body, schema) {
249
+ const errors = validateInputSchema(body, schema);
250
+ if (errors.length === 0)
251
+ return;
252
+ process.stderr.write(chalk_1.default.yellow(`\nInput validation warning:\n`) +
253
+ errors.map(e => chalk_1.default.yellow(` - ${e}`)).join('\n') +
254
+ chalk_1.default.yellow('\n\nThe request will still be sent, but may fail server-side.\n\n'));
255
+ }
191
256
  async function readStdin() {
192
257
  if (process.stdin.isTTY)
193
258
  return null;
@@ -853,7 +918,13 @@ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, cu
853
918
  try {
854
919
  const result = JSON.parse(stdout.trim());
855
920
  if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
856
- throw new errors_1.CliError(`Agent error: ${result.error}`);
921
+ const errorText = String(result.error);
922
+ const err = new errors_1.CliError(`Agent error: ${errorText}`);
923
+ // BUG-13: If stderr already showed this error during execution, don't print again
924
+ if (stderr.includes(errorText)) {
925
+ err.displayed = true;
926
+ }
927
+ throw err;
857
928
  }
858
929
  if (exitCode !== 0) {
859
930
  (0, output_1.printJson)(result);
@@ -871,6 +942,12 @@ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, cu
871
942
  }
872
943
  }
873
944
  else if (exitCode !== 0) {
945
+ // BUG-13: stderr was already relayed to user in real-time; don't embed it again
946
+ if (stderr.trim()) {
947
+ const err = new errors_1.CliError(`Agent exited with code ${exitCode}`);
948
+ err.displayed = true;
949
+ throw err;
950
+ }
874
951
  throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)\n\n` +
875
952
  `Common causes:\n` +
876
953
  ` - Missing LLM API key (check ${apiKeyEnvVar || 'API key env var'})\n` +
@@ -1177,7 +1254,13 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
1177
1254
  try {
1178
1255
  const result = JSON.parse(stdout.trim());
1179
1256
  if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
1180
- throw new errors_1.CliError(`Agent error: ${result.error}`);
1257
+ const errorText = String(result.error);
1258
+ const err = new errors_1.CliError(`Agent error: ${errorText}`);
1259
+ // BUG-13: If stderr already showed this error during execution, don't print again
1260
+ if (stderr.includes(errorText)) {
1261
+ err.displayed = true;
1262
+ }
1263
+ throw err;
1181
1264
  }
1182
1265
  if (exitCode !== 0) {
1183
1266
  (0, output_1.printJson)(result);
@@ -1195,8 +1278,11 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
1195
1278
  }
1196
1279
  }
1197
1280
  else if (exitCode !== 0) {
1281
+ // BUG-13: stderr was already relayed to user in real-time; don't embed it again
1198
1282
  if (stderr.trim()) {
1199
- throw new errors_1.CliError(`Agent exited with code ${exitCode}\n\nError output:\n${stderr.trim()}`);
1283
+ const err = new errors_1.CliError(`Agent exited with code ${exitCode}`);
1284
+ err.displayed = true;
1285
+ throw err;
1200
1286
  }
1201
1287
  throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)\n\n` +
1202
1288
  `Common causes:\n` +
@@ -1592,7 +1678,13 @@ async function executeLocalFromDir(dirPath, args, options) {
1592
1678
  try {
1593
1679
  const result = JSON.parse(stdout.trim());
1594
1680
  if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
1595
- throw new errors_1.CliError(`Agent error: ${result.error}`);
1681
+ const errorText = String(result.error);
1682
+ const err = new errors_1.CliError(`Agent error: ${errorText}`);
1683
+ // BUG-13: If stderr already showed this error during execution, don't print again
1684
+ if (stderr.includes(errorText)) {
1685
+ err.displayed = true;
1686
+ }
1687
+ throw err;
1596
1688
  }
1597
1689
  if (exitCode !== 0) {
1598
1690
  (0, output_1.printJson)(result);
@@ -1610,15 +1702,53 @@ async function executeLocalFromDir(dirPath, args, options) {
1610
1702
  }
1611
1703
  }
1612
1704
  else if (exitCode !== 0) {
1705
+ // BUG-13: stderr was already relayed to user in real-time; don't repeat
1706
+ if (stderr.trim()) {
1707
+ const err = new errors_1.CliError(`Agent exited with code ${exitCode}`);
1708
+ err.displayed = true;
1709
+ throw err;
1710
+ }
1613
1711
  throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
1614
1712
  }
1615
1713
  }
1616
1714
  // ─── Cloud execution path ───────────────────────────────────────────────────
1617
- function renderProgress(event) {
1715
+ function renderProgress(event, verbose = false) {
1618
1716
  switch (event.type) {
1619
1717
  case 'turn_start':
1620
1718
  process.stderr.write(chalk_1.default.gray(` Turn ${event.turn}/${event.max_turns}\n`));
1621
1719
  break;
1720
+ case 'sandbox_start':
1721
+ process.stderr.write(chalk_1.default.gray(' Starting sandbox...\n'));
1722
+ break;
1723
+ case 'setup_step': {
1724
+ const name = String(event.name || 'setup');
1725
+ const status = String(event.status || 'started');
1726
+ const icon = status === 'completed' ? chalk_1.default.green('✓') : status === 'failed' ? chalk_1.default.red('x') : chalk_1.default.gray('·');
1727
+ process.stderr.write(` ${icon} ${chalk_1.default.gray(name)} ${chalk_1.default.gray(status)}\n`);
1728
+ break;
1729
+ }
1730
+ case 'stdout': {
1731
+ if (!verbose)
1732
+ break;
1733
+ const text = typeof event.text === 'string' ? event.text : '';
1734
+ if (text)
1735
+ process.stderr.write(chalk_1.default.gray(text));
1736
+ break;
1737
+ }
1738
+ case 'stderr': {
1739
+ if (!verbose)
1740
+ break;
1741
+ const text = typeof event.text === 'string' ? event.text : '';
1742
+ if (text)
1743
+ process.stderr.write(chalk_1.default.yellow(text));
1744
+ break;
1745
+ }
1746
+ case 'heartbeat':
1747
+ // Keepalive for long-running silent steps; avoid noisy output.
1748
+ break;
1749
+ case 'output_truncated':
1750
+ process.stderr.write(chalk_1.default.yellow(' Live output truncated. Use `orch logs <run-id>` for full output.\n'));
1751
+ break;
1622
1752
  case 'tool_call': {
1623
1753
  const icon = event.tool === 'bash'
1624
1754
  ? '$'
@@ -1635,9 +1765,23 @@ function renderProgress(event) {
1635
1765
  if (event.status === 'error')
1636
1766
  process.stderr.write(chalk_1.default.yellow(` (error)\n`));
1637
1767
  break;
1638
- case 'done':
1768
+ case 'done': {
1639
1769
  process.stderr.write(chalk_1.default.green(` Done\n`));
1770
+ // In verbose mode, show exit code and execution time from done event
1771
+ if (verbose) {
1772
+ const parts = [];
1773
+ if (typeof event.exit_code === 'number' && event.exit_code !== 0) {
1774
+ parts.push(`exit_code=${event.exit_code}`);
1775
+ }
1776
+ if (typeof event.execution_time_ms === 'number') {
1777
+ parts.push(`${(event.execution_time_ms / 1000).toFixed(1)}s`);
1778
+ }
1779
+ if (parts.length > 0) {
1780
+ process.stderr.write(chalk_1.default.gray(` (${parts.join(', ')})\n`));
1781
+ }
1782
+ }
1640
1783
  break;
1784
+ }
1641
1785
  case 'error':
1642
1786
  process.stderr.write(chalk_1.default.red(` Error: ${event.message}\n`));
1643
1787
  break;
@@ -1696,6 +1840,50 @@ async function executeCloud(agentRef, file, options) {
1696
1840
  }
1697
1841
  }
1698
1842
  }
1843
+ // --estimate: show cost estimate before running and ask for confirmation
1844
+ if (options.estimate) {
1845
+ try {
1846
+ const est = await (0, api_1.getAgentCostEstimate)(resolved, org, parsed.agent, parsed.version);
1847
+ const e = est.estimate;
1848
+ if (e.sample_size === 0) {
1849
+ process.stderr.write(chalk_1.default.yellow('\nNo run history available for cost estimation.\n'));
1850
+ process.stderr.write(chalk_1.default.gray('This agent has not been run before — cost data will appear after first run.\n\n'));
1851
+ }
1852
+ else {
1853
+ const fmtUsd = (v) => {
1854
+ if (v < 0.001)
1855
+ return '<$0.001';
1856
+ if (v < 0.01)
1857
+ return `$${v.toFixed(4)}`;
1858
+ if (v < 1)
1859
+ return `$${v.toFixed(3)}`;
1860
+ return `$${v.toFixed(2)}`;
1861
+ };
1862
+ process.stderr.write('\n' + chalk_1.default.bold('Cost Estimate') + chalk_1.default.gray(` (${e.sample_size} runs, last ${e.period_days}d)`) + '\n');
1863
+ process.stderr.write(` Average: ${chalk_1.default.green(fmtUsd(e.avg_cost_usd))} · Median: ${chalk_1.default.green(fmtUsd(e.p50_cost_usd))} · 95th pct: ${chalk_1.default.yellow(fmtUsd(e.p95_cost_usd))}\n`);
1864
+ if (e.provider_breakdown && e.provider_breakdown.length > 0) {
1865
+ const parts = e.provider_breakdown.map(p => `${p.provider}: ${fmtUsd(p.avg_cost_usd)}`).join(', ');
1866
+ process.stderr.write(chalk_1.default.gray(` By provider: ${parts}\n`));
1867
+ }
1868
+ process.stderr.write('\n');
1869
+ }
1870
+ // Ask for confirmation
1871
+ const rl = await Promise.resolve().then(() => __importStar(require('readline')));
1872
+ const iface = rl.createInterface({ input: process.stdin, output: process.stderr });
1873
+ const answer = await new Promise(resolve => {
1874
+ iface.question('Proceed with run? [Y/n] ', resolve);
1875
+ });
1876
+ iface.close();
1877
+ if (answer.trim().toLowerCase() === 'n') {
1878
+ process.stderr.write('Run cancelled.\n');
1879
+ return;
1880
+ }
1881
+ }
1882
+ catch {
1883
+ // Non-fatal: if estimate fails, proceed with the run
1884
+ process.stderr.write(chalk_1.default.gray('Could not fetch cost estimate. Proceeding...\n\n'));
1885
+ }
1886
+ }
1699
1887
  const endpoint = options.endpoint?.trim() || agentMeta.default_endpoint || 'analyze';
1700
1888
  const headers = {
1701
1889
  Authorization: `Bearer ${resolved.apiKey}`,
@@ -1811,6 +1999,8 @@ async function executeCloud(agentRef, file, options) {
1811
1999
  mountArgs: options.mount,
1812
2000
  llmCredentials,
1813
2001
  });
2002
+ const injectedObj = JSON.parse(injected.body);
2003
+ warnInputSchemaErrors(injectedObj, agentMeta.input_schema);
1814
2004
  body = injected.body;
1815
2005
  sourceLabel = injected.sourceLabel;
1816
2006
  headers['Content-Type'] = 'application/json';
@@ -1851,6 +2041,7 @@ async function executeCloud(agentRef, file, options) {
1851
2041
  }
1852
2042
  }
1853
2043
  applySchemaDefaults(bodyObj, agentMeta.input_schema);
2044
+ warnInputSchemaErrors(bodyObj, agentMeta.input_schema);
1854
2045
  if (llmCredentials)
1855
2046
  bodyObj.llm_credentials = llmCredentials;
1856
2047
  body = JSON.stringify(bodyObj);
@@ -1872,14 +2063,12 @@ async function executeCloud(agentRef, file, options) {
1872
2063
  else if (options.data) {
1873
2064
  const resolvedBody = await resolveJsonBody(options.data);
1874
2065
  warnIfLocalPathReference(resolvedBody);
2066
+ const parsedBody = JSON.parse(resolvedBody);
2067
+ warnInputSchemaErrors(parsedBody, agentMeta.input_schema);
1875
2068
  if (llmCredentials) {
1876
- const bodyObj = JSON.parse(resolvedBody);
1877
- bodyObj.llm_credentials = llmCredentials;
1878
- body = JSON.stringify(bodyObj);
1879
- }
1880
- else {
1881
- body = resolvedBody;
2069
+ parsedBody.llm_credentials = llmCredentials;
1882
2070
  }
2071
+ body = JSON.stringify(parsedBody);
1883
2072
  headers['Content-Type'] = 'application/json';
1884
2073
  }
1885
2074
  else if ((filePaths.length > 0 || options.metadata) && cloudEngine !== 'code_runtime') {
@@ -1919,6 +2108,7 @@ async function executeCloud(agentRef, file, options) {
1919
2108
  }
1920
2109
  }
1921
2110
  applySchemaDefaults(bodyObj, agentMeta.input_schema);
2111
+ warnInputSchemaErrors(bodyObj, agentMeta.input_schema);
1922
2112
  if (llmCredentials) {
1923
2113
  bodyObj.llm_credentials = llmCredentials;
1924
2114
  }
@@ -1948,16 +2138,27 @@ async function executeCloud(agentRef, file, options) {
1948
2138
  } // end of non-injection path
1949
2139
  const verboseQs = options.verbose ? '?verbose=true' : '';
1950
2140
  const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}${verboseQs}`;
1951
- // Enable SSE streaming for managed-loop agents (unless --json or --no-stream or --output)
2141
+ // Enable SSE streaming for sandbox-backed engines (unless --json or --no-stream or --output)
1952
2142
  const isManagedLoopAgent = cloudType === 'agent' && cloudEngine === 'managed_loop';
1953
- const wantStream = isManagedLoopAgent && !options.json && !options.noStream && !options.output;
2143
+ const isCodeRuntimeAgent = cloudEngine === 'code_runtime';
2144
+ const streamCapable = isManagedLoopAgent || isCodeRuntimeAgent;
2145
+ const streamDisabled = options.stream === false || options.noStream === true;
2146
+ const wantStream = streamCapable && !options.json && !streamDisabled && !options.output;
1954
2147
  if (wantStream) {
1955
2148
  headers['Accept'] = 'text/event-stream';
1956
2149
  }
1957
- const spinner = options.json ? null : (0, spinner_1.createSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
2150
+ // Print verbose debug info before request
2151
+ if (options.verbose && !options.json) {
2152
+ process.stderr.write(chalk_1.default.gray(`\n[verbose] ${org}/${parsed.agent}@${parsed.version}\n` +
2153
+ `[verbose] type=${cloudType} engine=${cloudEngine} endpoint=${endpoint}\n` +
2154
+ `[verbose] stream=${wantStream ? 'yes' : 'no'} url=${url}\n`));
2155
+ }
2156
+ const { spinner, dispose: disposeSpinner } = options.json
2157
+ ? { spinner: null, dispose: () => { } }
2158
+ : (0, spinner_1.createElapsedSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
1958
2159
  spinner?.start();
1959
- // Managed-loop runs can take longer; use 10 min timeout for streaming.
1960
- const timeoutMs = isManagedLoopAgent ? 600000 : undefined;
2160
+ // Streamed sandbox runs can take longer; use 10 min timeout.
2161
+ const timeoutMs = wantStream ? 600000 : undefined;
1961
2162
  let response;
1962
2163
  try {
1963
2164
  response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
@@ -1990,7 +2191,7 @@ async function executeCloud(agentRef, file, options) {
1990
2191
  : undefined;
1991
2192
  throw new errors_1.CliError(`Your CLI version (${package_json_1.default.version}) is too old.\n\n` +
1992
2193
  (minVersion ? `Minimum required: ${minVersion}\n` : '') +
1993
- 'Update with: npm update -g @orchagent/cli');
2194
+ 'Update with: npm install -g @orchagent/cli@latest');
1994
2195
  }
1995
2196
  if (errorCode === 'LLM_KEY_REQUIRED') {
1996
2197
  spinner?.stop();
@@ -2021,6 +2222,14 @@ async function executeCloud(agentRef, file, options) {
2021
2222
  const hint = typeof payload === 'object' && payload
2022
2223
  ? payload.error?.hint
2023
2224
  : undefined;
2225
+ // Read error category from gateway (UX-7)
2226
+ const errorCategory = typeof payload === 'object' && payload
2227
+ ? payload.error?.category
2228
+ : undefined;
2229
+ // Read sandbox_exit_code for backward compat with older gateways
2230
+ const sandboxExitCode = typeof payload === 'object' && payload
2231
+ ? payload.metadata?.sandbox_exit_code
2232
+ : undefined;
2024
2233
  // Detect platform errors that surface as SANDBOX_ERROR (BUG-11)
2025
2234
  const lowerMessage = (message || '').toLowerCase();
2026
2235
  const isPlatformError = /\b403\b/.test(message || '') ||
@@ -2028,11 +2237,27 @@ async function executeCloud(agentRef, file, options) {
2028
2237
  lowerMessage.includes('proxy token') ||
2029
2238
  lowerMessage.includes('orchagent_service_key') ||
2030
2239
  lowerMessage.includes('orchagent_billing_org');
2031
- const attribution = isPlatformError
2032
- ? `This may be a platform configuration issue, not an error in the agent's code.\n` +
2033
- `If this persists, contact support with the ref below.`
2034
- : `This is an error in the agent's code, not the platform.\n` +
2035
- `Check the agent code and requirements, then republish.`;
2240
+ // Categorize: platform > gateway category > exit_code heuristic > neutral
2241
+ let attribution;
2242
+ if (isPlatformError) {
2243
+ attribution =
2244
+ `This may be a platform configuration issue, not an error in the agent's code.\n` +
2245
+ `If this persists, contact support with the ref below.`;
2246
+ }
2247
+ else if (errorCategory === 'code_error' || (!errorCategory && sandboxExitCode != null && sandboxExitCode !== 0)) {
2248
+ attribution =
2249
+ `This is an error in the agent's code, not the platform.\n` +
2250
+ `Check the agent code and requirements, then republish.`;
2251
+ }
2252
+ else if (errorCategory === 'setup_error') {
2253
+ attribution =
2254
+ `The agent failed during setup. This may be a dependency or configuration issue.\n` +
2255
+ `Check requirements.txt and environment configuration.`;
2256
+ }
2257
+ else {
2258
+ // No category from gateway, no exit code — can't determine blame
2259
+ attribution = `Agent execution failed. Check agent logs for details.`;
2260
+ }
2036
2261
  throw new errors_1.CliError(`${message}\n\n` +
2037
2262
  attribution +
2038
2263
  (hint ? `\n\nHint: ${hint}` : '') +
@@ -2095,11 +2320,22 @@ async function executeCloud(agentRef, file, options) {
2095
2320
  const { parseSSE } = await Promise.resolve().then(() => __importStar(require('../lib/sse.js')));
2096
2321
  let finalPayload = null;
2097
2322
  let hadError = false;
2098
- process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version}:\n`));
2323
+ if (options.verbose) {
2324
+ process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version} (verbose):\n`));
2325
+ process.stderr.write(chalk_1.default.gray(` type=${cloudType} engine=${cloudEngine} endpoint=${endpoint}\n`));
2326
+ }
2327
+ else {
2328
+ process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version}:\n`));
2329
+ }
2330
+ let progressErrorShown = false;
2099
2331
  for await (const { event, data } of parseSSE(response.body)) {
2100
2332
  if (event === 'progress') {
2101
2333
  try {
2102
- renderProgress(JSON.parse(data));
2334
+ const parsed = JSON.parse(data);
2335
+ renderProgress(parsed, !!options.verbose);
2336
+ if (parsed.type === 'error') {
2337
+ progressErrorShown = true;
2338
+ }
2103
2339
  }
2104
2340
  catch {
2105
2341
  // ignore malformed progress events
@@ -2134,7 +2370,12 @@ async function executeCloud(agentRef, file, options) {
2134
2370
  const errMsg = typeof finalPayload === 'object' && finalPayload
2135
2371
  ? finalPayload.error?.message || 'Agent execution failed'
2136
2372
  : 'Agent execution failed';
2137
- throw new errors_1.CliError(errMsg);
2373
+ const err = new errors_1.CliError(errMsg);
2374
+ // BUG-13: Error was already displayed by renderProgress during streaming
2375
+ if (progressErrorShown) {
2376
+ err.displayed = true;
2377
+ }
2378
+ throw err;
2138
2379
  }
2139
2380
  if (finalPayload !== null) {
2140
2381
  (0, output_1.printJson)(finalPayload);
@@ -2341,6 +2582,7 @@ async function executeLocal(agentRef, args, options) {
2341
2582
  catch {
2342
2583
  throw new errors_1.CliError('Invalid JSON input');
2343
2584
  }
2585
+ warnInputSchemaErrors(agentInputData, agentData.input_schema);
2344
2586
  // Write prompt to temp dir and run
2345
2587
  const tempAgentDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-${parsed.agent}-${Date.now()}`);
2346
2588
  await promises_1.default.mkdir(tempAgentDir, { recursive: true });
@@ -2466,6 +2708,7 @@ async function executeLocal(agentRef, args, options) {
2466
2708
  throw new errors_1.CliError('Invalid JSON input');
2467
2709
  }
2468
2710
  }
2711
+ warnInputSchemaErrors(inputData, agentData.input_schema);
2469
2712
  // Handle skill composition
2470
2713
  let skillPrompts = [];
2471
2714
  if (!options.noSkills) {
@@ -2495,14 +2738,15 @@ function registerRunCommand(program) {
2495
2738
  .option('--data <json>', 'JSON payload (string or @file, @- for stdin)')
2496
2739
  .option('--input <json>', 'Alias for --data')
2497
2740
  .option('--json', 'Output raw JSON')
2498
- .option('--verbose', 'Show sandbox stdout/stderr output (cloud only)')
2741
+ .option('--verbose', 'Show sandbox stdout/stderr and debug info (cloud only)')
2742
+ .option('--estimate', 'Show cost estimate before running and ask for confirmation')
2499
2743
  .option('--provider <provider>', 'LLM provider (openai, anthropic, gemini, ollama)')
2500
2744
  .option('--model <model>', 'LLM model to use (overrides agent default)')
2501
2745
  .option('--key <key>', 'LLM API key (overrides env vars)')
2502
2746
  .option('--skills <skills>', 'Add skills (comma-separated)')
2503
2747
  .option('--skills-only <skills>', 'Use only these skills')
2504
2748
  .option('--no-skills', 'Ignore default skills')
2505
- .option('--no-stream', 'Disable real-time streaming for agent-type agents')
2749
+ .option('--no-stream', 'Disable real-time streaming for stream-capable sandbox runs')
2506
2750
  // Cloud-only options
2507
2751
  .option('--endpoint <endpoint>', 'Override agent endpoint (cloud only)')
2508
2752
  .option('--tenant <tenant>', 'Tenant identifier for multi-tenant callers (cloud only)')
@@ -196,14 +196,16 @@ function registerScheduleCommand(program) {
196
196
  });
197
197
  const s = result.schedule;
198
198
  process.stdout.write(chalk_1.default.green('\u2713') + ` Schedule created\n\n`);
199
- process.stdout.write(` ID: ${s.id}\n`);
200
- process.stdout.write(` Agent: ${s.agent_name}@${s.agent_version}\n`);
201
- process.stdout.write(` Type: ${s.schedule_type}\n`);
199
+ process.stdout.write(` ID: ${s.id}\n`);
200
+ process.stdout.write(` Agent: ${s.agent_name}@${s.agent_version}\n`);
201
+ process.stdout.write(` Type: ${s.schedule_type}\n`);
202
+ process.stdout.write(` Enabled: ${chalk_1.default.green('yes')}\n`);
203
+ process.stdout.write(` Auto-update: ${s.auto_update === false ? chalk_1.default.yellow('no (pinned)') : chalk_1.default.green('yes')}\n`);
202
204
  if (s.schedule_type === 'cron') {
203
- process.stdout.write(` Cron: ${s.cron_expression}\n`);
204
- process.stdout.write(` TZ: ${s.timezone}\n`);
205
+ process.stdout.write(` Cron: ${s.cron_expression}\n`);
206
+ process.stdout.write(` Timezone: ${s.timezone}\n`);
205
207
  if (s.next_run_at) {
206
- process.stdout.write(` Next: ${formatDate(s.next_run_at)}\n`);
208
+ process.stdout.write(` Next run: ${formatDate(s.next_run_at)}\n`);
207
209
  }
208
210
  }
209
211
  else {
@@ -212,6 +214,9 @@ function registerScheduleCommand(program) {
212
214
  process.stdout.write(` ${s.webhook_url}\n`);
213
215
  }
214
216
  }
217
+ if (s.llm_provider) {
218
+ process.stdout.write(` Provider: ${s.llm_provider}\n`);
219
+ }
215
220
  process.stdout.write('\n');
216
221
  });
217
222
  // orch schedule update <schedule-id>
@@ -17,6 +17,7 @@ const dotenv_1 = require("../lib/dotenv");
17
17
  const config_1 = require("../lib/config");
18
18
  const llm_1 = require("../lib/llm");
19
19
  const bundle_1 = require("../lib/bundle");
20
+ const test_mock_runner_1 = require("../lib/test-mock-runner");
20
21
  // ─── Utility functions ───────────────────────────────────────────────────────
21
22
  function validateFixture(data, fixturePath) {
22
23
  const fileName = path_1.default.basename(fixturePath);
@@ -793,6 +794,13 @@ async function executeTests(agentDir, validation, testFiles, verbose, config) {
793
794
  process.stderr.write(chalk_1.default.gray(' "expected_contains": ["response"]\n'));
794
795
  process.stderr.write(chalk_1.default.gray(' }\n\n'));
795
796
  if (validation.executionEngine === 'managed_loop') {
797
+ process.stderr.write('For orchestrators with sub-agents, add mocked fixtures:\n');
798
+ process.stderr.write(chalk_1.default.gray(' # tests/fixture-mock-basic.json\n'));
799
+ process.stderr.write(chalk_1.default.gray(' {\n'));
800
+ process.stderr.write(chalk_1.default.gray(' "input": {"task": "..."},\n'));
801
+ process.stderr.write(chalk_1.default.gray(' "mocks": {"tool_name": {"key": "mock response"}},\n'));
802
+ process.stderr.write(chalk_1.default.gray(' "expected_contains": ["expected"]\n'));
803
+ process.stderr.write(chalk_1.default.gray(' }\n\n'));
796
804
  process.stderr.write('Or test the full agent loop:\n');
797
805
  process.stderr.write(chalk_1.default.gray(` orch run . --local --data '{"task": "..."}'\n\n`));
798
806
  }
@@ -833,8 +841,51 @@ async function executeTests(agentDir, validation, testFiles, verbose, config) {
833
841
  if (code !== 0)
834
842
  exitCode = 1;
835
843
  }
844
+ else if (validation.executionEngine === 'managed_loop') {
845
+ // For managed_loop agents, split fixtures: mocked vs regular
846
+ const mockedFixtures = [];
847
+ const regularFixtures = [];
848
+ for (const fixturePath of testFiles.fixtures) {
849
+ try {
850
+ const raw = await promises_1.default.readFile(fixturePath, 'utf-8');
851
+ const data = JSON.parse(raw);
852
+ if (data.mocks && typeof data.mocks === 'object' && !Array.isArray(data.mocks)) {
853
+ mockedFixtures.push(fixturePath);
854
+ }
855
+ else {
856
+ regularFixtures.push(fixturePath);
857
+ }
858
+ }
859
+ catch {
860
+ regularFixtures.push(fixturePath); // Let downstream validation handle errors
861
+ }
862
+ }
863
+ // Run mocked orchestration tests (full agent loop with mock sub-agents)
864
+ if (mockedFixtures.length > 0) {
865
+ try {
866
+ const manifestPath = path_1.default.join(agentDir, 'orchagent.json');
867
+ const manifestRaw = await promises_1.default.readFile(manifestPath, 'utf-8');
868
+ const manifest = JSON.parse(manifestRaw);
869
+ const code = await (0, test_mock_runner_1.runMockedAgentFixtureTests)(agentDir, mockedFixtures, manifest, verbose, config);
870
+ if (code !== 0)
871
+ exitCode = 1;
872
+ }
873
+ catch (err) {
874
+ if (err instanceof errors_1.CliError)
875
+ throw err;
876
+ process.stderr.write(chalk_1.default.red(` Error running mocked tests: ${err.message}\n`));
877
+ exitCode = 1;
878
+ }
879
+ }
880
+ // Run regular (non-mocked) fixtures as LLM-based prompt tests
881
+ if (regularFixtures.length > 0) {
882
+ const code = await runPromptFixtureTests(agentDir, regularFixtures, verbose, config);
883
+ if (code !== 0)
884
+ exitCode = 1;
885
+ }
886
+ }
836
887
  else {
837
- // Prompt, skill, and managed_loop agents: LLM-based fixture tests
888
+ // Prompt, skill agents: LLM-based fixture tests
838
889
  const code = await runPromptFixtureTests(agentDir, testFiles.fixtures, verbose, config);
839
890
  if (code !== 0)
840
891
  exitCode = 1;
@@ -1040,6 +1091,22 @@ Fixture Format (tests/fixture-basic.json):
1040
1091
  For code_runtime agents, fixtures run your entrypoint with input as stdin.
1041
1092
  For prompt/agent types, fixtures call the LLM with your prompt + input.
1042
1093
 
1094
+ Mocked Orchestration Tests (tests/fixture-mock-*.json):
1095
+ For agent-type (managed_loop) orchestrators, add "mocks" to test the full
1096
+ agent loop with deterministic sub-agent responses:
1097
+ {
1098
+ "description": "Orchestrator handles scan results",
1099
+ "input": {"code": "import os"},
1100
+ "mocks": {
1101
+ "scan_secrets": {"findings": [{"type": "code_injection"}]},
1102
+ "scan_deps": {"vulnerabilities": []}
1103
+ },
1104
+ "expected_contains": ["code_injection"]
1105
+ }
1106
+
1107
+ The LLM runs the full tool-use loop, but custom tool calls return mock
1108
+ responses instead of calling real sub-agents. Great for CI testing.
1109
+
1043
1110
  Run mode (--run):
1044
1111
  Validates the agent, then executes it once with the provided --data.
1045
1112
  Loads .env automatically. Same interface as: orch run . --local --data '...'