@samrahimi/smol-js 0.6.5 → 0.7.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.mjs CHANGED
@@ -710,9 +710,21 @@ Total time: ${(duration / 1e3).toFixed(2)}s`);
710
710
  getName() {
711
711
  return this.config.name;
712
712
  }
713
+ /** Get agent type identifier */
714
+ getType() {
715
+ return this.constructor.name;
716
+ }
717
+ /** Get max steps configuration */
718
+ getMaxSteps() {
719
+ return this.config.maxSteps;
720
+ }
721
+ /** Set the event callback (useful for orchestration) */
722
+ setOnEvent(callback) {
723
+ this.config.onEvent = callback;
724
+ }
713
725
  /** Sleep for a specified duration */
714
726
  sleep(ms) {
715
- return new Promise((resolve5) => setTimeout(resolve5, ms));
727
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
716
728
  }
717
729
  };
718
730
 
@@ -1381,12 +1393,12 @@ var UserInputTool = class extends Tool {
1381
1393
  input: process.stdin,
1382
1394
  output: process.stdout
1383
1395
  });
1384
- return new Promise((resolve5) => {
1396
+ return new Promise((resolve7) => {
1385
1397
  rl.question(`
1386
1398
  [Agent asks]: ${question}
1387
1399
  Your response: `, (answer) => {
1388
1400
  rl.close();
1389
- resolve5(answer);
1401
+ resolve7(answer);
1390
1402
  });
1391
1403
  });
1392
1404
  }
@@ -1865,6 +1877,10 @@ Please try a different approach.`
1865
1877
  memoryStep.tokenUsage = response.tokenUsage;
1866
1878
  if (response.content && response.content.trim()) {
1867
1879
  this.logger.reasoning(response.content.trim());
1880
+ this.emitEvent("agent_thinking", {
1881
+ step: this.currentStep,
1882
+ content: response.content.trim()
1883
+ });
1868
1884
  }
1869
1885
  if (!response.toolCalls || response.toolCalls.length === 0) {
1870
1886
  this.logger.warn("No tool calls in response. Prompting model to use tools.");
@@ -1897,42 +1913,84 @@ Please try a different approach.`
1897
1913
  async processToolCalls(toolCalls) {
1898
1914
  const results = [];
1899
1915
  const executeTool = async (tc) => {
1916
+ const startTime = Date.now();
1900
1917
  const toolName = tc.function.name;
1901
1918
  const tool = this.tools.get(toolName);
1902
1919
  if (!tool) {
1920
+ const error = `Unknown tool: ${toolName}. Available tools: ${Array.from(this.tools.keys()).join(", ")}`;
1921
+ this.emitEvent("agent_tool_result", {
1922
+ step: this.currentStep,
1923
+ toolCallId: tc.id,
1924
+ toolName,
1925
+ result: null,
1926
+ error,
1927
+ duration: Date.now() - startTime
1928
+ });
1903
1929
  return {
1904
1930
  toolCallId: tc.id,
1905
1931
  toolName,
1906
1932
  result: null,
1907
- error: `Unknown tool: ${toolName}. Available tools: ${Array.from(this.tools.keys()).join(", ")}`
1933
+ error
1908
1934
  };
1909
1935
  }
1910
1936
  let args;
1911
1937
  try {
1912
1938
  args = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
1913
1939
  } catch {
1940
+ const error = `Failed to parse tool arguments: ${tc.function.arguments}`;
1941
+ this.emitEvent("agent_tool_result", {
1942
+ step: this.currentStep,
1943
+ toolCallId: tc.id,
1944
+ toolName,
1945
+ result: null,
1946
+ error,
1947
+ duration: Date.now() - startTime
1948
+ });
1914
1949
  return {
1915
1950
  toolCallId: tc.id,
1916
1951
  toolName,
1917
1952
  result: null,
1918
- error: `Failed to parse tool arguments: ${tc.function.arguments}`
1953
+ error
1919
1954
  };
1920
1955
  }
1921
1956
  this.logger.info(` Calling tool: ${toolName}(${JSON.stringify(args).slice(0, 100)}...)`);
1922
- this.emitEvent("agent_tool_call", { tool: toolName, args });
1957
+ this.emitEvent("agent_tool_call", {
1958
+ step: this.currentStep,
1959
+ toolCallId: tc.id,
1960
+ toolName,
1961
+ arguments: args
1962
+ });
1923
1963
  try {
1924
1964
  const result = await tool.call(args);
1965
+ const duration = Date.now() - startTime;
1966
+ this.emitEvent("agent_tool_result", {
1967
+ step: this.currentStep,
1968
+ toolCallId: tc.id,
1969
+ toolName,
1970
+ result,
1971
+ duration
1972
+ });
1925
1973
  return {
1926
1974
  toolCallId: tc.id,
1927
1975
  toolName,
1928
1976
  result
1929
1977
  };
1930
1978
  } catch (error) {
1979
+ const errorMsg = `Tool execution error: ${error.message}`;
1980
+ const duration = Date.now() - startTime;
1981
+ this.emitEvent("agent_tool_result", {
1982
+ step: this.currentStep,
1983
+ toolCallId: tc.id,
1984
+ toolName,
1985
+ result: null,
1986
+ error: errorMsg,
1987
+ duration
1988
+ });
1931
1989
  return {
1932
1990
  toolCallId: tc.id,
1933
1991
  toolName,
1934
1992
  result: null,
1935
- error: `Tool execution error: ${error.message}`
1993
+ error: errorMsg
1936
1994
  };
1937
1995
  }
1938
1996
  };
@@ -2010,7 +2068,7 @@ var Model = class {
2010
2068
  };
2011
2069
 
2012
2070
  // src/models/OpenAIModel.ts
2013
- import OpenAI from "openai";
2071
+ import OpenAI from "openai/index.mjs";
2014
2072
  var DEFAULT_MODEL_ID = "anthropic/claude-sonnet-4.5";
2015
2073
  var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
2016
2074
  var DEFAULT_TIMEOUT = 12e4;
@@ -2599,40 +2657,35 @@ var ExaGetContentsTool = class extends Tool {
2599
2657
  // src/tools/ExaResearchTool.ts
2600
2658
  var ExaResearchTool = class extends Tool {
2601
2659
  name = "exa_research";
2602
- description = "Perform deep research on a single topic using Exa.ai. Searches for relevant sources, retrieves their content, and finds similar pages for comprehensive coverage. Returns a structured research summary with sources. Use this for thorough research on any topic.";
2660
+ description = "Perform comprehensive web research using Exa.ai's agentic research system. Provide natural-language instructions about what to research, and the AI research agent will plan searches, gather sources, extract facts, and synthesize findings into a detailed markdown report with citations. Ideal for deep research on any topic. Results typically complete in 20-90 seconds.";
2603
2661
  inputs = {
2604
- topic: {
2662
+ instructions: {
2605
2663
  type: "string",
2606
- description: "The research topic or question to investigate",
2664
+ description: "Natural-language task description of what to research (max 4096 characters). Be clear about what information you want, how research should be conducted, and what the output should contain.",
2607
2665
  required: true
2608
2666
  },
2609
- numSources: {
2610
- type: "number",
2611
- description: "Number of primary sources to retrieve (default: 5, max: 10)",
2612
- required: false,
2613
- default: 5
2614
- },
2615
- category: {
2667
+ model: {
2616
2668
  type: "string",
2617
- description: 'Optional category: "research paper", "news", "blog", "company"',
2618
- required: false
2619
- },
2620
- includeDomains: {
2621
- type: "array",
2622
- description: "Only include results from these domains",
2669
+ description: 'Research model: "exa-research-fast" (faster, cheaper), "exa-research" (balanced, default), or "exa-research-pro" (most thorough)',
2623
2670
  required: false
2624
2671
  },
2625
- startPublishedDate: {
2626
- type: "string",
2627
- description: "Only include results published after this date (ISO 8601)",
2672
+ outputSchema: {
2673
+ type: "object",
2674
+ description: "Optional JSON Schema to enforce structured output format (max 8 root fields, 5 levels deep). If omitted, returns markdown report.",
2628
2675
  required: false
2629
2676
  }
2630
2677
  };
2631
2678
  outputType = "string";
2632
2679
  apiKey;
2680
+ defaultModel;
2681
+ pollInterval;
2682
+ maxPollTime;
2633
2683
  constructor(config) {
2634
2684
  super();
2635
2685
  this.apiKey = config?.apiKey ?? process.env.EXA_API_KEY ?? "";
2686
+ this.defaultModel = config?.model ?? "exa-research";
2687
+ this.pollInterval = config?.pollInterval ?? 2e3;
2688
+ this.maxPollTime = config?.maxPollTime ?? 3e5;
2636
2689
  }
2637
2690
  async setup() {
2638
2691
  if (!this.apiKey) {
@@ -2641,97 +2694,390 @@ var ExaResearchTool = class extends Tool {
2641
2694
  this.isSetup = true;
2642
2695
  }
2643
2696
  async execute(args) {
2644
- const topic = args.topic;
2645
- const numSources = Math.min(args.numSources ?? 5, 10);
2646
- const searchBody = {
2647
- query: topic,
2648
- numResults: numSources,
2649
- type: "auto",
2650
- text: { maxCharacters: 3e3 }
2697
+ const instructions = args.instructions;
2698
+ const model = args.model ?? this.defaultModel;
2699
+ const outputSchema = args.outputSchema;
2700
+ if (!instructions || instructions.length > 4096) {
2701
+ throw new Error("Instructions are required and must be <= 4096 characters");
2702
+ }
2703
+ const createBody = {
2704
+ instructions,
2705
+ model
2651
2706
  };
2652
- if (args.category) searchBody.category = args.category;
2653
- if (args.includeDomains) searchBody.includeDomains = args.includeDomains;
2654
- if (args.startPublishedDate) searchBody.startPublishedDate = args.startPublishedDate;
2655
- const searchResponse = await fetch("https://api.exa.ai/search", {
2707
+ if (outputSchema) {
2708
+ createBody.outputSchema = outputSchema;
2709
+ }
2710
+ const createResponse = await fetch("https://api.exa.ai/research/v1", {
2656
2711
  method: "POST",
2657
2712
  headers: {
2658
2713
  "x-api-key": this.apiKey,
2659
2714
  "Content-Type": "application/json"
2660
2715
  },
2661
- body: JSON.stringify(searchBody)
2716
+ body: JSON.stringify(createBody)
2662
2717
  });
2663
- if (!searchResponse.ok) {
2664
- const errorText = await searchResponse.text();
2665
- throw new Error(`Exa research search failed (${searchResponse.status}): ${errorText}`);
2718
+ if (!createResponse.ok) {
2719
+ const errorText = await createResponse.text();
2720
+ throw new Error(`Failed to create research task (${createResponse.status}): ${errorText}`);
2666
2721
  }
2667
- const searchData = await searchResponse.json();
2668
- if (!searchData.results || searchData.results.length === 0) {
2669
- return `No research sources found for topic: "${topic}"`;
2670
- }
2671
- let similarResults = [];
2672
- if (searchData.results.length > 0) {
2673
- try {
2674
- const similarBody = {
2675
- url: searchData.results[0].url,
2676
- numResults: 3,
2677
- text: { maxCharacters: 2e3 }
2678
- };
2679
- const similarResponse = await fetch("https://api.exa.ai/findSimilar", {
2680
- method: "POST",
2681
- headers: {
2682
- "x-api-key": this.apiKey,
2683
- "Content-Type": "application/json"
2684
- },
2685
- body: JSON.stringify(similarBody)
2686
- });
2687
- if (similarResponse.ok) {
2688
- const similarData = await similarResponse.json();
2689
- similarResults = similarData.results ?? [];
2722
+ const createData = await createResponse.json();
2723
+ const researchId = createData.researchId;
2724
+ console.log(`Research task created: ${researchId}. Polling for results...`);
2725
+ const startTime = Date.now();
2726
+ let attempts = 0;
2727
+ while (Date.now() - startTime < this.maxPollTime) {
2728
+ attempts++;
2729
+ if (attempts > 1) {
2730
+ await new Promise((resolve7) => setTimeout(resolve7, this.pollInterval));
2731
+ }
2732
+ const statusResponse = await fetch(`https://api.exa.ai/research/v1/${researchId}`, {
2733
+ method: "GET",
2734
+ headers: {
2735
+ "x-api-key": this.apiKey
2690
2736
  }
2691
- } catch {
2737
+ });
2738
+ if (!statusResponse.ok) {
2739
+ const errorText = await statusResponse.text();
2740
+ throw new Error(`Failed to check research status (${statusResponse.status}): ${errorText}`);
2692
2741
  }
2693
- }
2694
- const allSources = [...searchData.results, ...similarResults];
2695
- const seenUrls = /* @__PURE__ */ new Set();
2696
- const uniqueSources = allSources.filter((s) => {
2697
- if (seenUrls.has(s.url)) return false;
2698
- seenUrls.add(s.url);
2699
- return true;
2700
- });
2701
- const sections = [];
2702
- sections.push(`# Research: ${topic}
2703
- `);
2704
- sections.push(`Found ${uniqueSources.length} sources.
2705
- `);
2706
- sections.push("## Key Sources\n");
2707
- for (let i = 0; i < uniqueSources.length; i++) {
2708
- const source = uniqueSources[i];
2709
- sections.push(`### ${i + 1}. ${source.title ?? "Untitled"}`);
2710
- sections.push(`URL: ${source.url}`);
2711
- if ("publishedDate" in source && source.publishedDate) {
2712
- sections.push(`Date: ${source.publishedDate}`);
2742
+ const statusData = await statusResponse.json();
2743
+ if (statusData.status === "completed") {
2744
+ const output = statusData.output;
2745
+ if (!output) {
2746
+ throw new Error("Research completed but no output was returned");
2747
+ }
2748
+ const sections = [];
2749
+ if (outputSchema && output.parsed) {
2750
+ sections.push("# Research Results (Structured)\n");
2751
+ sections.push(JSON.stringify(output.parsed, null, 2));
2752
+ } else {
2753
+ sections.push(output.content);
2754
+ }
2755
+ if (statusData.costDollars) {
2756
+ const cost = statusData.costDollars;
2757
+ sections.push("\n---\n");
2758
+ sections.push("## Research Metrics\n");
2759
+ sections.push(`- **Cost**: $${cost.total.toFixed(4)}`);
2760
+ sections.push(`- **Searches**: ${cost.numSearches}`);
2761
+ sections.push(`- **Pages analyzed**: ${cost.numPages}`);
2762
+ sections.push(`- **Reasoning tokens**: ${cost.reasoningTokens.toLocaleString()}`);
2763
+ }
2764
+ console.log(`Research completed in ${attempts} polls (${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
2765
+ return sections.join("\n");
2766
+ }
2767
+ if (statusData.status === "failed") {
2768
+ throw new Error(`Research failed: ${statusData.error ?? "Unknown error"}`);
2713
2769
  }
2714
- if ("author" in source && source.author) {
2715
- sections.push(`Author: ${source.author}`);
2770
+ if (statusData.status === "canceled") {
2771
+ throw new Error("Research was canceled");
2716
2772
  }
2717
- if (source.text) {
2718
- sections.push(`
2719
- Content:
2720
- ${source.text.slice(0, 2e3)}`);
2773
+ if (attempts % 10 === 0) {
2774
+ console.log(`Still researching... (${attempts} polls, ${((Date.now() - startTime) / 1e3).toFixed(0)}s elapsed)`);
2721
2775
  }
2722
- sections.push("");
2723
2776
  }
2724
- sections.push("## Source URLs\n");
2725
- uniqueSources.forEach((s, i) => {
2726
- sections.push(`${i + 1}. ${s.url}`);
2777
+ throw new Error(`Research timed out after ${this.maxPollTime / 1e3}s. Task ID: ${researchId}`);
2778
+ }
2779
+ };
2780
+
2781
+ // src/tools/ProxyTool.ts
2782
+ import { spawn } from "child_process";
2783
+ import * as path6 from "path";
2784
+
2785
+ // src/utils/bunInstaller.ts
2786
+ import { execSync } from "child_process";
2787
+ import * as path5 from "path";
2788
+ import * as fs5 from "fs";
2789
+ import * as os3 from "os";
2790
+ var cachedBunPath = null;
2791
+ async function ensureBunAvailable() {
2792
+ if (cachedBunPath) return cachedBunPath;
2793
+ const fromPath = whichBun();
2794
+ if (fromPath) {
2795
+ cachedBunPath = fromPath;
2796
+ return fromPath;
2797
+ }
2798
+ const localPath = path5.join(os3.homedir(), ".bun", "bin", "bun");
2799
+ if (fs5.existsSync(localPath)) {
2800
+ cachedBunPath = localPath;
2801
+ return localPath;
2802
+ }
2803
+ console.log(
2804
+ "\n[smol-js] Bun is required to run custom tools but was not found. Installing Bun automatically...\n"
2805
+ );
2806
+ try {
2807
+ execSync("curl --proto =https --tlsv1.2 -sSf https://bun.sh | bash", {
2808
+ stdio: "inherit",
2809
+ shell: "/bin/bash",
2810
+ env: { ...process.env, HOME: os3.homedir() }
2811
+ });
2812
+ } catch (err) {
2813
+ throw new Error(
2814
+ `[smol-js] Failed to auto-install Bun. Please install it manually: https://bun.sh
2815
+ Details: ${err.message}`
2816
+ );
2817
+ }
2818
+ const afterInstall = whichBun() || (fs5.existsSync(localPath) ? localPath : null);
2819
+ if (!afterInstall) {
2820
+ throw new Error(
2821
+ "[smol-js] Bun installation appeared to succeed but the binary was not found. Please install manually: https://bun.sh"
2822
+ );
2823
+ }
2824
+ console.log(`[smol-js] Bun installed successfully at: ${afterInstall}
2825
+ `);
2826
+ cachedBunPath = afterInstall;
2827
+ return afterInstall;
2828
+ }
2829
+ function whichBun() {
2830
+ try {
2831
+ const cmd = process.platform === "win32" ? "where bun" : "which bun";
2832
+ const result = execSync(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
2833
+ const first = result.split("\n")[0]?.trim();
2834
+ if (first && fs5.existsSync(first)) return first;
2835
+ return null;
2836
+ } catch {
2837
+ return null;
2838
+ }
2839
+ }
2840
+
2841
+ // src/tools/ProxyTool.ts
2842
+ var TOOL_RESULT_PREFIX = "[TOOL_RESULT]";
2843
+ var TOOL_ERROR_PREFIX = "[TOOL_ERROR]";
2844
+ var DEFAULT_TOOL_TIMEOUT_MS = 6e4;
2845
+ function resolveHarnessPath() {
2846
+ return path6.resolve(__dirname, "..", "toolHarness.ts");
2847
+ }
2848
+ var ProxyTool = class extends Tool {
2849
+ name;
2850
+ description;
2851
+ inputs;
2852
+ outputType;
2853
+ toolPath;
2854
+ timeout;
2855
+ bunPath = null;
2856
+ harnessPath = null;
2857
+ constructor(config) {
2858
+ super();
2859
+ this.name = config.name;
2860
+ this.description = config.description;
2861
+ this.inputs = config.inputs;
2862
+ this.outputType = config.outputType;
2863
+ this.toolPath = config.toolPath;
2864
+ this.timeout = config.timeout ?? DEFAULT_TOOL_TIMEOUT_MS;
2865
+ }
2866
+ /**
2867
+ * Ensure Bun is available and locate the harness before first invocation.
2868
+ */
2869
+ async setup() {
2870
+ this.bunPath = await ensureBunAvailable();
2871
+ this.harnessPath = resolveHarnessPath();
2872
+ this.isSetup = true;
2873
+ }
2874
+ /**
2875
+ * Spawn the harness in a Bun child process. The harness imports the tool,
2876
+ * calls execute(args), and writes the protocol lines. Any console.log from
2877
+ * the tool flows through stdout as plain lines.
2878
+ */
2879
+ async execute(args) {
2880
+ if (!this.bunPath || !this.harnessPath) {
2881
+ await this.setup();
2882
+ }
2883
+ const serializedArgs = JSON.stringify(args);
2884
+ return new Promise((resolve7, reject) => {
2885
+ const child = spawn(this.bunPath, ["run", this.harnessPath, this.toolPath, serializedArgs], {
2886
+ stdio: ["pipe", "pipe", "pipe"],
2887
+ env: { ...process.env }
2888
+ });
2889
+ let result = void 0;
2890
+ let resultReceived = false;
2891
+ let errorMessage = null;
2892
+ const logBuffer = [];
2893
+ let stderr = "";
2894
+ let partialLine = "";
2895
+ child.stdout.on("data", (chunk) => {
2896
+ partialLine += chunk.toString("utf8");
2897
+ const lines = partialLine.split("\n");
2898
+ partialLine = lines.pop();
2899
+ for (const line of lines) {
2900
+ this.processLine(line, {
2901
+ onOutput: (msg) => logBuffer.push(msg),
2902
+ onResult: (value) => {
2903
+ result = value;
2904
+ resultReceived = true;
2905
+ },
2906
+ onError: (msg) => {
2907
+ errorMessage = msg;
2908
+ }
2909
+ });
2910
+ }
2911
+ });
2912
+ child.stderr.on("data", (chunk) => {
2913
+ stderr += chunk.toString("utf8");
2914
+ });
2915
+ const timer = setTimeout(() => {
2916
+ child.kill("SIGTERM");
2917
+ reject(new Error(
2918
+ `Custom tool "${this.name}" timed out after ${this.timeout}ms. The process was terminated. Check the tool for infinite loops or slow operations.`
2919
+ ));
2920
+ }, this.timeout);
2921
+ timer.unref();
2922
+ child.on("close", (code) => {
2923
+ clearTimeout(timer);
2924
+ if (partialLine.trim()) {
2925
+ this.processLine(partialLine, {
2926
+ onOutput: (msg) => logBuffer.push(msg),
2927
+ onResult: (value) => {
2928
+ result = value;
2929
+ resultReceived = true;
2930
+ },
2931
+ onError: (msg) => {
2932
+ errorMessage = msg;
2933
+ }
2934
+ });
2935
+ }
2936
+ if (errorMessage) {
2937
+ reject(new Error(
2938
+ `Custom tool "${this.name}" reported an error: ${errorMessage}`
2939
+ ));
2940
+ return;
2941
+ }
2942
+ if (resultReceived) {
2943
+ if (logBuffer.length > 0) {
2944
+ const logPrefix = `[Tool output logs]
2945
+ ${logBuffer.join("\n")}
2946
+
2947
+ [Tool result]
2948
+ `;
2949
+ if (typeof result === "string") {
2950
+ resolve7(logPrefix + result);
2951
+ } else {
2952
+ resolve7({ logs: logBuffer.join("\n"), result });
2953
+ }
2954
+ } else {
2955
+ resolve7(result);
2956
+ }
2957
+ return;
2958
+ }
2959
+ const combined = (logBuffer.join("\n") + "\n" + stderr).trim();
2960
+ if (code !== 0) {
2961
+ reject(new Error(
2962
+ `Custom tool "${this.name}" exited with code ${code}. Output: ${combined || "(none)"}`
2963
+ ));
2964
+ } else {
2965
+ resolve7(combined || `Tool "${this.name}" produced no output.`);
2966
+ }
2967
+ });
2968
+ child.on("error", (err) => {
2969
+ clearTimeout(timer);
2970
+ reject(new Error(
2971
+ `Failed to spawn custom tool "${this.name}": ${err.message}`
2972
+ ));
2973
+ });
2727
2974
  });
2728
- return sections.join("\n");
2975
+ }
2976
+ // --- line parser: protocol is spoken by harness, interpreted here ---
2977
+ processLine(line, handlers) {
2978
+ const trimmed = line.trimEnd();
2979
+ if (!trimmed) return;
2980
+ if (trimmed.startsWith(TOOL_RESULT_PREFIX)) {
2981
+ const json = trimmed.slice(TOOL_RESULT_PREFIX.length).trim();
2982
+ try {
2983
+ handlers.onResult(JSON.parse(json));
2984
+ } catch {
2985
+ handlers.onResult(json);
2986
+ }
2987
+ } else if (trimmed.startsWith(TOOL_ERROR_PREFIX)) {
2988
+ handlers.onError(trimmed.slice(TOOL_ERROR_PREFIX.length).trim());
2989
+ } else {
2990
+ handlers.onOutput(trimmed);
2991
+ }
2729
2992
  }
2730
2993
  };
2731
2994
 
2995
+ // src/tools/CustomToolScanner.ts
2996
+ import * as fs6 from "fs";
2997
+ import * as path7 from "path";
2998
+ var METADATA_REGEX = /export\s+const\s+TOOL_METADATA\s*=\s*(\{[\s\S]*?\});\s*$/m;
2999
+ function scanCustomTools(folderPath) {
3000
+ if (!fs6.existsSync(folderPath)) {
3001
+ throw new Error(
3002
+ `Custom tools folder not found: ${folderPath}. Create the directory or check your --custom-tools-folder path.`
3003
+ );
3004
+ }
3005
+ const entries = fs6.readdirSync(folderPath, { withFileTypes: true });
3006
+ const discovered = [];
3007
+ for (const entry of entries) {
3008
+ if (entry.isDirectory()) continue;
3009
+ const ext = path7.extname(entry.name).toLowerCase();
3010
+ if (ext !== ".ts" && ext !== ".js") continue;
3011
+ const filePath = path7.resolve(folderPath, entry.name);
3012
+ const baseName = path7.basename(entry.name, ext);
3013
+ let metadata;
3014
+ try {
3015
+ metadata = extractMetadata(filePath);
3016
+ } catch (err) {
3017
+ throw new Error(
3018
+ `Failed to extract TOOL_METADATA from "${entry.name}": ${err.message}
3019
+ Ensure the file exports \`export const TOOL_METADATA = { name, description, inputs, outputType };\``
3020
+ );
3021
+ }
3022
+ if (metadata.name !== baseName) {
3023
+ throw new Error(
3024
+ `Tool metadata name mismatch in "${entry.name}": file is "${baseName}" but TOOL_METADATA.name is "${metadata.name}". They must match (Convention over Configuration).`
3025
+ );
3026
+ }
3027
+ discovered.push({ filePath, metadata });
3028
+ }
3029
+ return discovered;
3030
+ }
3031
+ function extractMetadata(filePath) {
3032
+ const source = fs6.readFileSync(filePath, "utf8");
3033
+ const match = source.match(METADATA_REGEX);
3034
+ if (!match) {
3035
+ throw new Error(
3036
+ "No `export const TOOL_METADATA = { ... };` block found. Add the metadata export at the bottom of your tool file."
3037
+ );
3038
+ }
3039
+ let parsed;
3040
+ try {
3041
+ parsed = new Function(`"use strict"; return (${match[1]});`)();
3042
+ } catch (err) {
3043
+ throw new Error(
3044
+ `Could not parse TOOL_METADATA object: ${err.message}. Ensure it is a valid JavaScript object literal.`
3045
+ );
3046
+ }
3047
+ if (!parsed.name || typeof parsed.name !== "string") {
3048
+ throw new Error("TOOL_METADATA.name must be a non-empty string.");
3049
+ }
3050
+ if (!parsed.description || typeof parsed.description !== "string") {
3051
+ throw new Error("TOOL_METADATA.description must be a non-empty string.");
3052
+ }
3053
+ if (!parsed.inputs || typeof parsed.inputs !== "object") {
3054
+ throw new Error("TOOL_METADATA.inputs must be an object mapping parameter names to their schemas.");
3055
+ }
3056
+ if (!parsed.outputType || typeof parsed.outputType !== "string") {
3057
+ throw new Error("TOOL_METADATA.outputType must be a non-empty string.");
3058
+ }
3059
+ return parsed;
3060
+ }
3061
+ function loadCustomTools(folderPath) {
3062
+ const discovered = scanCustomTools(folderPath);
3063
+ const tools = /* @__PURE__ */ new Map();
3064
+ for (const { filePath, metadata } of discovered) {
3065
+ const config = {
3066
+ toolPath: filePath,
3067
+ name: metadata.name,
3068
+ description: metadata.description,
3069
+ inputs: metadata.inputs,
3070
+ outputType: metadata.outputType,
3071
+ timeout: metadata.timeout
3072
+ };
3073
+ tools.set(metadata.name, new ProxyTool(config));
3074
+ }
3075
+ return tools;
3076
+ }
3077
+
2732
3078
  // src/orchestrator/YAMLLoader.ts
2733
- import * as fs5 from "fs";
2734
- import * as path5 from "path";
3079
+ import * as fs7 from "fs";
3080
+ import * as path8 from "path";
2735
3081
  import YAML from "yaml";
2736
3082
  var TOOL_REGISTRY = {
2737
3083
  read_file: ReadFileTool,
@@ -2744,21 +3090,30 @@ var TOOL_REGISTRY = {
2744
3090
  };
2745
3091
  var YAMLLoader = class {
2746
3092
  customTools = /* @__PURE__ */ new Map();
3093
+ toolInstances = /* @__PURE__ */ new Map();
2747
3094
  /**
2748
- * Register a custom tool type for use in YAML definitions.
3095
+ * Register a custom tool type (class) for use in YAML definitions.
2749
3096
  */
2750
3097
  registerToolType(typeName, toolClass) {
2751
3098
  this.customTools.set(typeName, toolClass);
2752
3099
  }
3100
+ /**
3101
+ * Register a pre-built tool instance for use in YAML definitions.
3102
+ * Used by the custom tool system to register ProxyTool instances
3103
+ * that are created by the scanner rather than by class instantiation.
3104
+ */
3105
+ registerToolInstance(typeName, tool) {
3106
+ this.toolInstances.set(typeName, tool);
3107
+ }
2753
3108
  /**
2754
3109
  * Load a workflow from a YAML file path.
2755
3110
  */
2756
3111
  loadFromFile(filePath) {
2757
- const absolutePath = path5.isAbsolute(filePath) ? filePath : path5.resolve(process.cwd(), filePath);
2758
- if (!fs5.existsSync(absolutePath)) {
3112
+ const absolutePath = path8.isAbsolute(filePath) ? filePath : path8.resolve(process.cwd(), filePath);
3113
+ if (!fs7.existsSync(absolutePath)) {
2759
3114
  throw new Error(`Workflow file not found: ${absolutePath}`);
2760
3115
  }
2761
- const content = fs5.readFileSync(absolutePath, "utf-8");
3116
+ const content = fs7.readFileSync(absolutePath, "utf-8");
2762
3117
  return this.loadFromString(content);
2763
3118
  }
2764
3119
  /**
@@ -2833,9 +3188,16 @@ var YAMLLoader = class {
2833
3188
  * Build a tool instance from a type name and config.
2834
3189
  */
2835
3190
  buildTool(name, type, config) {
3191
+ const existingInstance = this.toolInstances.get(type);
3192
+ if (existingInstance) {
3193
+ if (name !== type && name !== existingInstance.name) {
3194
+ Object.defineProperty(existingInstance, "name", { value: name, writable: false });
3195
+ }
3196
+ return existingInstance;
3197
+ }
2836
3198
  const ToolClass = TOOL_REGISTRY[type] ?? this.customTools.get(type);
2837
3199
  if (!ToolClass) {
2838
- throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys()].join(", ")}`);
3200
+ throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys(), ...this.toolInstances.keys()].join(", ")}`);
2839
3201
  }
2840
3202
  const tool = new ToolClass(config);
2841
3203
  if (name !== type && name !== tool.name) {
@@ -2863,11 +3225,16 @@ var YAMLLoader = class {
2863
3225
  if (tool) {
2864
3226
  agentTools.push(tool);
2865
3227
  } else {
2866
- const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
2867
- if (ToolClass) {
2868
- agentTools.push(new ToolClass());
3228
+ const instance = this.toolInstances.get(toolName);
3229
+ if (instance) {
3230
+ agentTools.push(instance);
2869
3231
  } else {
2870
- throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
3232
+ const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
3233
+ if (ToolClass) {
3234
+ agentTools.push(new ToolClass());
3235
+ } else {
3236
+ throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
3237
+ }
2871
3238
  }
2872
3239
  }
2873
3240
  }
@@ -2918,39 +3285,117 @@ var YAMLLoader = class {
2918
3285
 
2919
3286
  // src/orchestrator/Orchestrator.ts
2920
3287
  import chalk2 from "chalk";
3288
+
3289
+ // src/output/JSONOutputHandler.ts
3290
+ var JSONOutputHandler = class {
3291
+ runId;
3292
+ startTime;
3293
+ constructor(config) {
3294
+ this.runId = config.runId;
3295
+ this.startTime = Date.now();
3296
+ }
3297
+ emit(type, data, extra = {}) {
3298
+ console.log(JSON.stringify({
3299
+ runId: this.runId,
3300
+ timestamp: Date.now(),
3301
+ type,
3302
+ ...extra,
3303
+ data
3304
+ }));
3305
+ }
3306
+ emitRunStart(workflowPath, task, cwd) {
3307
+ this.emit("run_start", { workflowPath, task, cwd });
3308
+ }
3309
+ emitWorkflowLoaded(name, description, agents, tools, entrypoint) {
3310
+ this.emit("workflow_loaded", { name, description, agents, tools, entrypoint });
3311
+ }
3312
+ emitRunEnd(success, output, totalTokens, totalSteps) {
3313
+ this.emit("run_end", {
3314
+ success,
3315
+ output,
3316
+ totalDuration: Date.now() - this.startTime,
3317
+ totalTokens,
3318
+ totalSteps
3319
+ });
3320
+ }
3321
+ emitAgentStart(agentName, depth, task, agentType, maxSteps) {
3322
+ this.emit("agent_start", { task, agentType, maxSteps }, { agentName, depth });
3323
+ }
3324
+ emitAgentEnd(agentName, depth, output, totalSteps, tokenUsage, duration, success) {
3325
+ this.emit("agent_end", { output, totalSteps, tokenUsage, duration, success }, { agentName, depth });
3326
+ }
3327
+ emitAgentStep(agentName, depth, stepNumber, maxSteps, phase) {
3328
+ this.emit("agent_step", { stepNumber, maxSteps, phase }, { agentName, depth });
3329
+ }
3330
+ emitAgentThinking(agentName, depth, stepNumber, content, isPartial) {
3331
+ this.emit("agent_thinking", { stepNumber, content, isPartial }, { agentName, depth });
3332
+ }
3333
+ emitToolCall(agentName, depth, stepNumber, toolCallId, toolName, args) {
3334
+ this.emit("agent_tool_call", { stepNumber, toolCallId, toolName, arguments: args }, { agentName, depth });
3335
+ }
3336
+ emitToolResult(agentName, depth, stepNumber, toolCallId, toolName, result, error, duration) {
3337
+ this.emit("agent_tool_result", { stepNumber, toolCallId, toolName, result, error, duration }, { agentName, depth });
3338
+ }
3339
+ emitObservation(agentName, depth, stepNumber, observation, codeAction, logs) {
3340
+ this.emit("agent_observation", { stepNumber, observation, codeAction, logs }, { agentName, depth });
3341
+ }
3342
+ emitError(message, stack, agentName, depth, stepNumber) {
3343
+ this.emit("error", { message, stack, stepNumber }, {
3344
+ ...agentName ? { agentName } : {},
3345
+ ...depth !== void 0 ? { depth } : {}
3346
+ });
3347
+ }
3348
+ };
3349
+
3350
+ // src/orchestrator/Orchestrator.ts
2921
3351
  var Orchestrator = class {
2922
3352
  loader;
2923
3353
  config;
2924
3354
  activeAgents = /* @__PURE__ */ new Map();
2925
3355
  eventLog = [];
3356
+ jsonOutput = null;
3357
+ isJsonMode = false;
2926
3358
  constructor(config = {}) {
2927
3359
  this.loader = new YAMLLoader();
3360
+ this.isJsonMode = config.outputFormat === "json";
2928
3361
  this.config = {
2929
3362
  verbose: config.verbose ?? true,
2930
- onEvent: config.onEvent
3363
+ onEvent: config.onEvent,
3364
+ outputFormat: config.outputFormat ?? "text",
3365
+ runId: config.runId,
3366
+ cwd: config.cwd
2931
3367
  };
3368
+ if (this.isJsonMode) {
3369
+ if (!config.runId) {
3370
+ throw new Error("runId is required for JSON output mode");
3371
+ }
3372
+ this.jsonOutput = new JSONOutputHandler({
3373
+ runId: config.runId,
3374
+ verbose: config.verbose ?? true
3375
+ });
3376
+ }
2932
3377
  }
2933
3378
  /**
2934
3379
  * Load a workflow from a YAML file.
2935
3380
  */
2936
3381
  loadWorkflow(filePath) {
2937
3382
  const workflow = this.loader.loadFromFile(filePath);
2938
- this.displayWorkflowInfo(workflow);
3383
+ this.displayWorkflowInfo(workflow, filePath);
2939
3384
  return workflow;
2940
3385
  }
2941
3386
  /**
2942
3387
  * Load a workflow from YAML string.
2943
3388
  */
2944
- loadWorkflowFromString(yamlContent) {
3389
+ loadWorkflowFromString(yamlContent, sourcePath) {
2945
3390
  const workflow = this.loader.loadFromString(yamlContent);
2946
- this.displayWorkflowInfo(workflow);
3391
+ this.displayWorkflowInfo(workflow, sourcePath);
2947
3392
  return workflow;
2948
3393
  }
2949
3394
  /**
2950
3395
  * Run a loaded workflow with a task.
2951
3396
  */
2952
- async runWorkflow(workflow, task) {
2953
- this.displayRunStart(workflow.name, task);
3397
+ async runWorkflow(workflow, task, workflowPath) {
3398
+ this.displayRunStart(workflow.name, task, workflowPath);
2954
3399
  this.instrumentAgent(workflow.entrypointAgent, workflow.entrypointAgent.getName(), 0);
2955
3400
  for (const [name, agent] of workflow.agents) {
2956
3401
  if (agent !== workflow.entrypointAgent) {
@@ -2959,10 +3404,13 @@ var Orchestrator = class {
2959
3404
  }
2960
3405
  try {
2961
3406
  const result = await workflow.entrypointAgent.run(task);
2962
- this.displayRunEnd(result);
3407
+ this.displayRunEnd(result, true);
2963
3408
  return result;
2964
3409
  } catch (error) {
2965
3410
  this.displayError(error);
3411
+ if (this.isJsonMode && this.jsonOutput) {
3412
+ this.jsonOutput.emitRunEnd(false, null, 0, 0);
3413
+ }
2966
3414
  throw error;
2967
3415
  }
2968
3416
  }
@@ -2979,11 +3427,128 @@ var Orchestrator = class {
2979
3427
  */
2980
3428
  instrumentAgent(agent, name, depth) {
2981
3429
  this.activeAgents.set(name, { agent, depth });
3430
+ agent.setOnEvent((event) => {
3431
+ const orchestratorEvent = {
3432
+ type: event.type,
3433
+ agentName: name,
3434
+ depth,
3435
+ data: event.data,
3436
+ timestamp: Date.now()
3437
+ };
3438
+ this.logEvent(orchestratorEvent);
3439
+ if (this.isJsonMode && this.jsonOutput) {
3440
+ this.emitAgentEventAsJSON(event, name, depth);
3441
+ }
3442
+ });
3443
+ }
3444
+ /**
3445
+ * Emit an agent event as JSON.
3446
+ */
3447
+ emitAgentEventAsJSON(event, agentName, depth) {
3448
+ if (!this.jsonOutput) return;
3449
+ const data = event.data;
3450
+ switch (event.type) {
3451
+ case "agent_start":
3452
+ this.jsonOutput.emitAgentStart(
3453
+ agentName,
3454
+ depth,
3455
+ data.task,
3456
+ data.name ? "ToolUseAgent" : "CodeAgent",
3457
+ // Will be improved
3458
+ 20
3459
+ // Default maxSteps, could be passed
3460
+ );
3461
+ break;
3462
+ case "agent_step":
3463
+ this.jsonOutput.emitAgentStep(
3464
+ agentName,
3465
+ depth,
3466
+ data.step,
3467
+ data.maxSteps,
3468
+ "start"
3469
+ );
3470
+ break;
3471
+ case "agent_thinking":
3472
+ this.jsonOutput.emitAgentThinking(
3473
+ agentName,
3474
+ depth,
3475
+ data.step,
3476
+ data.content,
3477
+ false
3478
+ );
3479
+ break;
3480
+ case "agent_tool_call":
3481
+ this.jsonOutput.emitToolCall(
3482
+ agentName,
3483
+ depth,
3484
+ data.step,
3485
+ data.toolCallId,
3486
+ data.toolName,
3487
+ data.arguments
3488
+ );
3489
+ break;
3490
+ case "agent_tool_result":
3491
+ this.jsonOutput.emitToolResult(
3492
+ agentName,
3493
+ depth,
3494
+ data.step,
3495
+ data.toolCallId,
3496
+ data.toolName,
3497
+ data.result,
3498
+ data.error,
3499
+ data.duration
3500
+ );
3501
+ break;
3502
+ case "agent_observation":
3503
+ this.jsonOutput.emitObservation(
3504
+ agentName,
3505
+ depth,
3506
+ data.step,
3507
+ data.observation,
3508
+ data.codeAction,
3509
+ data.logs
3510
+ );
3511
+ break;
3512
+ case "agent_error":
3513
+ this.jsonOutput.emitError(
3514
+ data.error,
3515
+ void 0,
3516
+ agentName,
3517
+ depth,
3518
+ data.step
3519
+ );
3520
+ break;
3521
+ case "agent_end":
3522
+ this.jsonOutput.emitAgentEnd(
3523
+ agentName,
3524
+ depth,
3525
+ data.output,
3526
+ 0,
3527
+ // totalSteps - would need to track
3528
+ data.tokenUsage,
3529
+ data.duration,
3530
+ true
3531
+ );
3532
+ break;
3533
+ }
2982
3534
  }
2983
3535
  /**
2984
3536
  * Display workflow info at startup.
2985
3537
  */
2986
- displayWorkflowInfo(workflow) {
3538
+ displayWorkflowInfo(workflow, _sourcePath) {
3539
+ const agents = Array.from(workflow.agents.keys());
3540
+ const tools = Array.from(workflow.tools.keys());
3541
+ const entrypoint = workflow.entrypointAgent.getName();
3542
+ if (this.isJsonMode && this.jsonOutput) {
3543
+ this.jsonOutput.emitWorkflowLoaded(
3544
+ workflow.name,
3545
+ workflow.description,
3546
+ agents,
3547
+ tools,
3548
+ entrypoint
3549
+ );
3550
+ return;
3551
+ }
2987
3552
  if (!this.config.verbose) return;
2988
3553
  const line = "\u2550".repeat(70);
2989
3554
  console.log(chalk2.cyan(line));
@@ -2991,16 +3556,20 @@ var Orchestrator = class {
2991
3556
  if (workflow.description) {
2992
3557
  console.log(chalk2.cyan(` ${workflow.description}`));
2993
3558
  }
2994
- console.log(chalk2.cyan(` Agents: ${Array.from(workflow.agents.keys()).join(", ")}`));
2995
- console.log(chalk2.cyan(` Tools: ${Array.from(workflow.tools.keys()).join(", ") || "(none defined at workflow level)"}`));
2996
- console.log(chalk2.cyan(` Entrypoint: ${workflow.entrypointAgent.getName()}`));
3559
+ console.log(chalk2.cyan(` Agents: ${agents.join(", ")}`));
3560
+ console.log(chalk2.cyan(` Tools: ${tools.join(", ") || "(none defined at workflow level)"}`));
3561
+ console.log(chalk2.cyan(` Entrypoint: ${entrypoint}`));
2997
3562
  console.log(chalk2.cyan(line));
2998
3563
  console.log();
2999
3564
  }
3000
3565
  /**
3001
3566
  * Display run start info.
3002
3567
  */
3003
- displayRunStart(workflowName, task) {
3568
+ displayRunStart(workflowName, task, workflowPath) {
3569
+ if (this.isJsonMode && this.jsonOutput) {
3570
+ this.jsonOutput.emitRunStart(workflowPath || workflowName, task, this.config.cwd);
3571
+ return;
3572
+ }
3004
3573
  if (!this.config.verbose) return;
3005
3574
  console.log(chalk2.green.bold(`
3006
3575
  \u25B6 Running workflow "${workflowName}"`));
@@ -3010,7 +3579,16 @@ var Orchestrator = class {
3010
3579
  /**
3011
3580
  * Display run completion info.
3012
3581
  */
3013
- displayRunEnd(result) {
3582
+ displayRunEnd(result, success = true) {
3583
+ if (this.isJsonMode && this.jsonOutput) {
3584
+ this.jsonOutput.emitRunEnd(
3585
+ success,
3586
+ result.output,
3587
+ result.tokenUsage.totalTokens,
3588
+ result.steps.length
3589
+ );
3590
+ return;
3591
+ }
3014
3592
  if (!this.config.verbose) return;
3015
3593
  console.log(chalk2.gray("\n" + "\u2500".repeat(70)));
3016
3594
  console.log(chalk2.green.bold(`
@@ -3028,6 +3606,10 @@ var Orchestrator = class {
3028
3606
  * Display an error.
3029
3607
  */
3030
3608
  displayError(error) {
3609
+ if (this.isJsonMode && this.jsonOutput) {
3610
+ this.jsonOutput.emitError(error.message, error.stack);
3611
+ return;
3612
+ }
3031
3613
  if (!this.config.verbose) return;
3032
3614
  console.error(chalk2.red.bold(`
3033
3615
  \u274C Workflow failed: ${error.message}`));
@@ -3056,6 +3638,24 @@ var Orchestrator = class {
3056
3638
  getLoader() {
3057
3639
  return this.loader;
3058
3640
  }
3641
+ /**
3642
+ * Get the JSON output handler (if in JSON mode).
3643
+ */
3644
+ getJSONOutputHandler() {
3645
+ return this.jsonOutput;
3646
+ }
3647
+ /**
3648
+ * Check if in JSON output mode.
3649
+ */
3650
+ isJSONOutputMode() {
3651
+ return this.isJsonMode;
3652
+ }
3653
+ /**
3654
+ * Get the run ID.
3655
+ */
3656
+ getRunId() {
3657
+ return this.config.runId;
3658
+ }
3059
3659
  };
3060
3660
  export {
3061
3661
  Agent,
@@ -3069,11 +3669,13 @@ export {
3069
3669
  ExaSearchTool,
3070
3670
  FINAL_ANSWER_PROMPT,
3071
3671
  FinalAnswerTool,
3672
+ JSONOutputHandler,
3072
3673
  LocalExecutor,
3073
3674
  LogLevel,
3074
3675
  Model,
3075
3676
  OpenAIModel,
3076
3677
  Orchestrator,
3678
+ ProxyTool,
3077
3679
  ReadFileTool,
3078
3680
  Tool,
3079
3681
  ToolUseAgent,
@@ -3086,6 +3688,8 @@ export {
3086
3688
  formatToolDescriptions,
3087
3689
  generateSystemPrompt,
3088
3690
  generateToolUseSystemPrompt,
3089
- getErrorRecoveryPrompt
3691
+ getErrorRecoveryPrompt,
3692
+ loadCustomTools,
3693
+ scanCustomTools
3090
3694
  };
3091
3695
  //# sourceMappingURL=index.mjs.map