@samrahimi/smol-js 0.6.5 → 0.7.0

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((resolve6) => setTimeout(resolve6, 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((resolve6) => {
1385
1397
  rl.question(`
1386
1398
  [Agent asks]: ${question}
1387
1399
  Your response: `, (answer) => {
1388
1400
  rl.close();
1389
- resolve5(answer);
1401
+ resolve6(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,386 @@ 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((resolve6) => setTimeout(resolve6, 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
+
2784
+ // src/utils/bunInstaller.ts
2785
+ import { execSync } from "child_process";
2786
+ import * as path5 from "path";
2787
+ import * as fs5 from "fs";
2788
+ import * as os3 from "os";
2789
+ var cachedBunPath = null;
2790
+ async function ensureBunAvailable() {
2791
+ if (cachedBunPath) return cachedBunPath;
2792
+ const fromPath = whichBun();
2793
+ if (fromPath) {
2794
+ cachedBunPath = fromPath;
2795
+ return fromPath;
2796
+ }
2797
+ const localPath = path5.join(os3.homedir(), ".bun", "bin", "bun");
2798
+ if (fs5.existsSync(localPath)) {
2799
+ cachedBunPath = localPath;
2800
+ return localPath;
2801
+ }
2802
+ console.log(
2803
+ "\n[smol-js] Bun is required to run custom tools but was not found. Installing Bun automatically...\n"
2804
+ );
2805
+ try {
2806
+ execSync("curl --proto =https --tlsv1.2 -sSf https://bun.sh | bash", {
2807
+ stdio: "inherit",
2808
+ shell: "/bin/bash",
2809
+ env: { ...process.env, HOME: os3.homedir() }
2727
2810
  });
2728
- return sections.join("\n");
2811
+ } catch (err) {
2812
+ throw new Error(
2813
+ `[smol-js] Failed to auto-install Bun. Please install it manually: https://bun.sh
2814
+ Details: ${err.message}`
2815
+ );
2816
+ }
2817
+ const afterInstall = whichBun() || (fs5.existsSync(localPath) ? localPath : null);
2818
+ if (!afterInstall) {
2819
+ throw new Error(
2820
+ "[smol-js] Bun installation appeared to succeed but the binary was not found. Please install manually: https://bun.sh"
2821
+ );
2822
+ }
2823
+ console.log(`[smol-js] Bun installed successfully at: ${afterInstall}
2824
+ `);
2825
+ cachedBunPath = afterInstall;
2826
+ return afterInstall;
2827
+ }
2828
+ function whichBun() {
2829
+ try {
2830
+ const cmd = process.platform === "win32" ? "where bun" : "which bun";
2831
+ const result = execSync(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
2832
+ const first = result.split("\n")[0]?.trim();
2833
+ if (first && fs5.existsSync(first)) return first;
2834
+ return null;
2835
+ } catch {
2836
+ return null;
2837
+ }
2838
+ }
2839
+
2840
+ // src/tools/ProxyTool.ts
2841
+ var TOOL_OUTPUT_PREFIX = "[TOOL_OUTPUT]";
2842
+ var TOOL_RESULT_PREFIX = "[TOOL_RESULT]";
2843
+ var TOOL_ERROR_PREFIX = "[TOOL_ERROR]";
2844
+ var DEFAULT_TOOL_TIMEOUT_MS = 6e4;
2845
+ var ProxyTool = class extends Tool {
2846
+ name;
2847
+ description;
2848
+ inputs;
2849
+ outputType;
2850
+ toolPath;
2851
+ timeout;
2852
+ bunPath = null;
2853
+ constructor(config) {
2854
+ super();
2855
+ this.name = config.name;
2856
+ this.description = config.description;
2857
+ this.inputs = config.inputs;
2858
+ this.outputType = config.outputType;
2859
+ this.toolPath = config.toolPath;
2860
+ this.timeout = config.timeout ?? DEFAULT_TOOL_TIMEOUT_MS;
2861
+ }
2862
+ /**
2863
+ * Ensure Bun is available before first invocation.
2864
+ */
2865
+ async setup() {
2866
+ this.bunPath = await ensureBunAvailable();
2867
+ this.isSetup = true;
2868
+ }
2869
+ /**
2870
+ * Spawn the tool in a Bun child process, pass serialized args via CLI,
2871
+ * stream stdout back as log lines, and parse the final result.
2872
+ */
2873
+ async execute(args) {
2874
+ if (!this.bunPath) {
2875
+ await this.setup();
2876
+ }
2877
+ const serializedArgs = JSON.stringify(args);
2878
+ return new Promise((resolve6, reject) => {
2879
+ const child = spawn(this.bunPath, ["run", this.toolPath, serializedArgs], {
2880
+ stdio: ["pipe", "pipe", "pipe"],
2881
+ env: { ...process.env }
2882
+ });
2883
+ let result = void 0;
2884
+ let resultReceived = false;
2885
+ let errorMessage = null;
2886
+ const logBuffer = [];
2887
+ let stderr = "";
2888
+ let partialLine = "";
2889
+ child.stdout.on("data", (chunk) => {
2890
+ partialLine += chunk.toString("utf8");
2891
+ const lines = partialLine.split("\n");
2892
+ partialLine = lines.pop();
2893
+ for (const line of lines) {
2894
+ this.processLine(line, {
2895
+ onOutput: (msg) => logBuffer.push(msg),
2896
+ onResult: (value) => {
2897
+ result = value;
2898
+ resultReceived = true;
2899
+ },
2900
+ onError: (msg) => {
2901
+ errorMessage = msg;
2902
+ }
2903
+ });
2904
+ }
2905
+ });
2906
+ child.stderr.on("data", (chunk) => {
2907
+ stderr += chunk.toString("utf8");
2908
+ });
2909
+ const timer = setTimeout(() => {
2910
+ child.kill("SIGTERM");
2911
+ reject(new Error(
2912
+ `Custom tool "${this.name}" timed out after ${this.timeout}ms. The process was terminated. Check the tool for infinite loops or slow operations.`
2913
+ ));
2914
+ }, this.timeout);
2915
+ timer.unref();
2916
+ child.on("close", (code) => {
2917
+ clearTimeout(timer);
2918
+ if (partialLine.trim()) {
2919
+ this.processLine(partialLine, {
2920
+ onOutput: (msg) => logBuffer.push(msg),
2921
+ onResult: (value) => {
2922
+ result = value;
2923
+ resultReceived = true;
2924
+ },
2925
+ onError: (msg) => {
2926
+ errorMessage = msg;
2927
+ }
2928
+ });
2929
+ }
2930
+ if (errorMessage) {
2931
+ reject(new Error(
2932
+ `Custom tool "${this.name}" reported an error: ${errorMessage}`
2933
+ ));
2934
+ return;
2935
+ }
2936
+ if (resultReceived) {
2937
+ if (logBuffer.length > 0) {
2938
+ const logPrefix = `[Tool output logs]
2939
+ ${logBuffer.join("\n")}
2940
+
2941
+ [Tool result]
2942
+ `;
2943
+ if (typeof result === "string") {
2944
+ resolve6(logPrefix + result);
2945
+ } else {
2946
+ resolve6({ logs: logBuffer.join("\n"), result });
2947
+ }
2948
+ } else {
2949
+ resolve6(result);
2950
+ }
2951
+ return;
2952
+ }
2953
+ const combined = (logBuffer.join("\n") + "\n" + stderr).trim();
2954
+ if (code !== 0) {
2955
+ reject(new Error(
2956
+ `Custom tool "${this.name}" exited with code ${code}. Output: ${combined || "(none)"}`
2957
+ ));
2958
+ } else {
2959
+ resolve6(combined || `Tool "${this.name}" produced no output.`);
2960
+ }
2961
+ });
2962
+ child.on("error", (err) => {
2963
+ clearTimeout(timer);
2964
+ reject(new Error(
2965
+ `Failed to spawn custom tool "${this.name}": ${err.message}`
2966
+ ));
2967
+ });
2968
+ });
2969
+ }
2970
+ // --- internal line parser ---
2971
+ processLine(line, handlers) {
2972
+ const trimmed = line.trimEnd();
2973
+ if (!trimmed) return;
2974
+ if (trimmed.startsWith(TOOL_RESULT_PREFIX)) {
2975
+ const json = trimmed.slice(TOOL_RESULT_PREFIX.length).trim();
2976
+ try {
2977
+ handlers.onResult(JSON.parse(json));
2978
+ } catch {
2979
+ handlers.onResult(json);
2980
+ }
2981
+ } else if (trimmed.startsWith(TOOL_ERROR_PREFIX)) {
2982
+ handlers.onError(trimmed.slice(TOOL_ERROR_PREFIX.length).trim());
2983
+ } else if (trimmed.startsWith(TOOL_OUTPUT_PREFIX)) {
2984
+ handlers.onOutput(trimmed.slice(TOOL_OUTPUT_PREFIX.length).trim());
2985
+ } else {
2986
+ handlers.onOutput(trimmed);
2987
+ }
2729
2988
  }
2730
2989
  };
2731
2990
 
2991
+ // src/tools/CustomToolScanner.ts
2992
+ import * as fs6 from "fs";
2993
+ import * as path6 from "path";
2994
+ var METADATA_REGEX = /export\s+const\s+TOOL_METADATA\s*=\s*(\{[\s\S]*?\});\s*$/m;
2995
+ function scanCustomTools(folderPath) {
2996
+ if (!fs6.existsSync(folderPath)) {
2997
+ throw new Error(
2998
+ `Custom tools folder not found: ${folderPath}. Create the directory or check your --custom-tools-folder path.`
2999
+ );
3000
+ }
3001
+ const entries = fs6.readdirSync(folderPath, { withFileTypes: true });
3002
+ const discovered = [];
3003
+ for (const entry of entries) {
3004
+ if (entry.isDirectory()) continue;
3005
+ const ext = path6.extname(entry.name).toLowerCase();
3006
+ if (ext !== ".ts" && ext !== ".js") continue;
3007
+ const filePath = path6.resolve(folderPath, entry.name);
3008
+ const baseName = path6.basename(entry.name, ext);
3009
+ let metadata;
3010
+ try {
3011
+ metadata = extractMetadata(filePath);
3012
+ } catch (err) {
3013
+ throw new Error(
3014
+ `Failed to extract TOOL_METADATA from "${entry.name}": ${err.message}
3015
+ Ensure the file exports \`export const TOOL_METADATA = { name, description, inputs, outputType };\``
3016
+ );
3017
+ }
3018
+ if (metadata.name !== baseName) {
3019
+ throw new Error(
3020
+ `Tool metadata name mismatch in "${entry.name}": file is "${baseName}" but TOOL_METADATA.name is "${metadata.name}". They must match (Convention over Configuration).`
3021
+ );
3022
+ }
3023
+ discovered.push({ filePath, metadata });
3024
+ }
3025
+ return discovered;
3026
+ }
3027
+ function extractMetadata(filePath) {
3028
+ const source = fs6.readFileSync(filePath, "utf8");
3029
+ const match = source.match(METADATA_REGEX);
3030
+ if (!match) {
3031
+ throw new Error(
3032
+ "No `export const TOOL_METADATA = { ... };` block found. Add the metadata export at the bottom of your tool file."
3033
+ );
3034
+ }
3035
+ let parsed;
3036
+ try {
3037
+ parsed = new Function(`"use strict"; return (${match[1]});`)();
3038
+ } catch (err) {
3039
+ throw new Error(
3040
+ `Could not parse TOOL_METADATA object: ${err.message}. Ensure it is a valid JavaScript object literal.`
3041
+ );
3042
+ }
3043
+ if (!parsed.name || typeof parsed.name !== "string") {
3044
+ throw new Error("TOOL_METADATA.name must be a non-empty string.");
3045
+ }
3046
+ if (!parsed.description || typeof parsed.description !== "string") {
3047
+ throw new Error("TOOL_METADATA.description must be a non-empty string.");
3048
+ }
3049
+ if (!parsed.inputs || typeof parsed.inputs !== "object") {
3050
+ throw new Error("TOOL_METADATA.inputs must be an object mapping parameter names to their schemas.");
3051
+ }
3052
+ if (!parsed.outputType || typeof parsed.outputType !== "string") {
3053
+ throw new Error("TOOL_METADATA.outputType must be a non-empty string.");
3054
+ }
3055
+ return parsed;
3056
+ }
3057
+ function loadCustomTools(folderPath) {
3058
+ const discovered = scanCustomTools(folderPath);
3059
+ const tools = /* @__PURE__ */ new Map();
3060
+ for (const { filePath, metadata } of discovered) {
3061
+ const config = {
3062
+ toolPath: filePath,
3063
+ name: metadata.name,
3064
+ description: metadata.description,
3065
+ inputs: metadata.inputs,
3066
+ outputType: metadata.outputType,
3067
+ timeout: metadata.timeout
3068
+ };
3069
+ tools.set(metadata.name, new ProxyTool(config));
3070
+ }
3071
+ return tools;
3072
+ }
3073
+
2732
3074
  // src/orchestrator/YAMLLoader.ts
2733
- import * as fs5 from "fs";
2734
- import * as path5 from "path";
3075
+ import * as fs7 from "fs";
3076
+ import * as path7 from "path";
2735
3077
  import YAML from "yaml";
2736
3078
  var TOOL_REGISTRY = {
2737
3079
  read_file: ReadFileTool,
@@ -2744,21 +3086,30 @@ var TOOL_REGISTRY = {
2744
3086
  };
2745
3087
  var YAMLLoader = class {
2746
3088
  customTools = /* @__PURE__ */ new Map();
3089
+ toolInstances = /* @__PURE__ */ new Map();
2747
3090
  /**
2748
- * Register a custom tool type for use in YAML definitions.
3091
+ * Register a custom tool type (class) for use in YAML definitions.
2749
3092
  */
2750
3093
  registerToolType(typeName, toolClass) {
2751
3094
  this.customTools.set(typeName, toolClass);
2752
3095
  }
3096
+ /**
3097
+ * Register a pre-built tool instance for use in YAML definitions.
3098
+ * Used by the custom tool system to register ProxyTool instances
3099
+ * that are created by the scanner rather than by class instantiation.
3100
+ */
3101
+ registerToolInstance(typeName, tool) {
3102
+ this.toolInstances.set(typeName, tool);
3103
+ }
2753
3104
  /**
2754
3105
  * Load a workflow from a YAML file path.
2755
3106
  */
2756
3107
  loadFromFile(filePath) {
2757
- const absolutePath = path5.isAbsolute(filePath) ? filePath : path5.resolve(process.cwd(), filePath);
2758
- if (!fs5.existsSync(absolutePath)) {
3108
+ const absolutePath = path7.isAbsolute(filePath) ? filePath : path7.resolve(process.cwd(), filePath);
3109
+ if (!fs7.existsSync(absolutePath)) {
2759
3110
  throw new Error(`Workflow file not found: ${absolutePath}`);
2760
3111
  }
2761
- const content = fs5.readFileSync(absolutePath, "utf-8");
3112
+ const content = fs7.readFileSync(absolutePath, "utf-8");
2762
3113
  return this.loadFromString(content);
2763
3114
  }
2764
3115
  /**
@@ -2833,9 +3184,16 @@ var YAMLLoader = class {
2833
3184
  * Build a tool instance from a type name and config.
2834
3185
  */
2835
3186
  buildTool(name, type, config) {
3187
+ const existingInstance = this.toolInstances.get(type);
3188
+ if (existingInstance) {
3189
+ if (name !== type && name !== existingInstance.name) {
3190
+ Object.defineProperty(existingInstance, "name", { value: name, writable: false });
3191
+ }
3192
+ return existingInstance;
3193
+ }
2836
3194
  const ToolClass = TOOL_REGISTRY[type] ?? this.customTools.get(type);
2837
3195
  if (!ToolClass) {
2838
- throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys()].join(", ")}`);
3196
+ throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys(), ...this.toolInstances.keys()].join(", ")}`);
2839
3197
  }
2840
3198
  const tool = new ToolClass(config);
2841
3199
  if (name !== type && name !== tool.name) {
@@ -2863,11 +3221,16 @@ var YAMLLoader = class {
2863
3221
  if (tool) {
2864
3222
  agentTools.push(tool);
2865
3223
  } else {
2866
- const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
2867
- if (ToolClass) {
2868
- agentTools.push(new ToolClass());
3224
+ const instance = this.toolInstances.get(toolName);
3225
+ if (instance) {
3226
+ agentTools.push(instance);
2869
3227
  } else {
2870
- throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
3228
+ const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
3229
+ if (ToolClass) {
3230
+ agentTools.push(new ToolClass());
3231
+ } else {
3232
+ throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
3233
+ }
2871
3234
  }
2872
3235
  }
2873
3236
  }
@@ -2918,39 +3281,117 @@ var YAMLLoader = class {
2918
3281
 
2919
3282
  // src/orchestrator/Orchestrator.ts
2920
3283
  import chalk2 from "chalk";
3284
+
3285
+ // src/output/JSONOutputHandler.ts
3286
+ var JSONOutputHandler = class {
3287
+ runId;
3288
+ startTime;
3289
+ constructor(config) {
3290
+ this.runId = config.runId;
3291
+ this.startTime = Date.now();
3292
+ }
3293
+ emit(type, data, extra = {}) {
3294
+ console.log(JSON.stringify({
3295
+ runId: this.runId,
3296
+ timestamp: Date.now(),
3297
+ type,
3298
+ ...extra,
3299
+ data
3300
+ }));
3301
+ }
3302
+ emitRunStart(workflowPath, task, cwd) {
3303
+ this.emit("run_start", { workflowPath, task, cwd });
3304
+ }
3305
+ emitWorkflowLoaded(name, description, agents, tools, entrypoint) {
3306
+ this.emit("workflow_loaded", { name, description, agents, tools, entrypoint });
3307
+ }
3308
+ emitRunEnd(success, output, totalTokens, totalSteps) {
3309
+ this.emit("run_end", {
3310
+ success,
3311
+ output,
3312
+ totalDuration: Date.now() - this.startTime,
3313
+ totalTokens,
3314
+ totalSteps
3315
+ });
3316
+ }
3317
+ emitAgentStart(agentName, depth, task, agentType, maxSteps) {
3318
+ this.emit("agent_start", { task, agentType, maxSteps }, { agentName, depth });
3319
+ }
3320
+ emitAgentEnd(agentName, depth, output, totalSteps, tokenUsage, duration, success) {
3321
+ this.emit("agent_end", { output, totalSteps, tokenUsage, duration, success }, { agentName, depth });
3322
+ }
3323
+ emitAgentStep(agentName, depth, stepNumber, maxSteps, phase) {
3324
+ this.emit("agent_step", { stepNumber, maxSteps, phase }, { agentName, depth });
3325
+ }
3326
+ emitAgentThinking(agentName, depth, stepNumber, content, isPartial) {
3327
+ this.emit("agent_thinking", { stepNumber, content, isPartial }, { agentName, depth });
3328
+ }
3329
+ emitToolCall(agentName, depth, stepNumber, toolCallId, toolName, args) {
3330
+ this.emit("agent_tool_call", { stepNumber, toolCallId, toolName, arguments: args }, { agentName, depth });
3331
+ }
3332
+ emitToolResult(agentName, depth, stepNumber, toolCallId, toolName, result, error, duration) {
3333
+ this.emit("agent_tool_result", { stepNumber, toolCallId, toolName, result, error, duration }, { agentName, depth });
3334
+ }
3335
+ emitObservation(agentName, depth, stepNumber, observation, codeAction, logs) {
3336
+ this.emit("agent_observation", { stepNumber, observation, codeAction, logs }, { agentName, depth });
3337
+ }
3338
+ emitError(message, stack, agentName, depth, stepNumber) {
3339
+ this.emit("error", { message, stack, stepNumber }, {
3340
+ ...agentName ? { agentName } : {},
3341
+ ...depth !== void 0 ? { depth } : {}
3342
+ });
3343
+ }
3344
+ };
3345
+
3346
+ // src/orchestrator/Orchestrator.ts
2921
3347
  var Orchestrator = class {
2922
3348
  loader;
2923
3349
  config;
2924
3350
  activeAgents = /* @__PURE__ */ new Map();
2925
3351
  eventLog = [];
3352
+ jsonOutput = null;
3353
+ isJsonMode = false;
2926
3354
  constructor(config = {}) {
2927
3355
  this.loader = new YAMLLoader();
3356
+ this.isJsonMode = config.outputFormat === "json";
2928
3357
  this.config = {
2929
3358
  verbose: config.verbose ?? true,
2930
- onEvent: config.onEvent
3359
+ onEvent: config.onEvent,
3360
+ outputFormat: config.outputFormat ?? "text",
3361
+ runId: config.runId,
3362
+ cwd: config.cwd
2931
3363
  };
3364
+ if (this.isJsonMode) {
3365
+ if (!config.runId) {
3366
+ throw new Error("runId is required for JSON output mode");
3367
+ }
3368
+ this.jsonOutput = new JSONOutputHandler({
3369
+ runId: config.runId,
3370
+ verbose: config.verbose ?? true
3371
+ });
3372
+ }
2932
3373
  }
2933
3374
  /**
2934
3375
  * Load a workflow from a YAML file.
2935
3376
  */
2936
3377
  loadWorkflow(filePath) {
2937
3378
  const workflow = this.loader.loadFromFile(filePath);
2938
- this.displayWorkflowInfo(workflow);
3379
+ this.displayWorkflowInfo(workflow, filePath);
2939
3380
  return workflow;
2940
3381
  }
2941
3382
  /**
2942
3383
  * Load a workflow from YAML string.
2943
3384
  */
2944
- loadWorkflowFromString(yamlContent) {
3385
+ loadWorkflowFromString(yamlContent, sourcePath) {
2945
3386
  const workflow = this.loader.loadFromString(yamlContent);
2946
- this.displayWorkflowInfo(workflow);
3387
+ this.displayWorkflowInfo(workflow, sourcePath);
2947
3388
  return workflow;
2948
3389
  }
2949
3390
  /**
2950
3391
  * Run a loaded workflow with a task.
2951
3392
  */
2952
- async runWorkflow(workflow, task) {
2953
- this.displayRunStart(workflow.name, task);
3393
+ async runWorkflow(workflow, task, workflowPath) {
3394
+ this.displayRunStart(workflow.name, task, workflowPath);
2954
3395
  this.instrumentAgent(workflow.entrypointAgent, workflow.entrypointAgent.getName(), 0);
2955
3396
  for (const [name, agent] of workflow.agents) {
2956
3397
  if (agent !== workflow.entrypointAgent) {
@@ -2959,10 +3400,13 @@ var Orchestrator = class {
2959
3400
  }
2960
3401
  try {
2961
3402
  const result = await workflow.entrypointAgent.run(task);
2962
- this.displayRunEnd(result);
3403
+ this.displayRunEnd(result, true);
2963
3404
  return result;
2964
3405
  } catch (error) {
2965
3406
  this.displayError(error);
3407
+ if (this.isJsonMode && this.jsonOutput) {
3408
+ this.jsonOutput.emitRunEnd(false, null, 0, 0);
3409
+ }
2966
3410
  throw error;
2967
3411
  }
2968
3412
  }
@@ -2979,11 +3423,128 @@ var Orchestrator = class {
2979
3423
  */
2980
3424
  instrumentAgent(agent, name, depth) {
2981
3425
  this.activeAgents.set(name, { agent, depth });
3426
+ agent.setOnEvent((event) => {
3427
+ const orchestratorEvent = {
3428
+ type: event.type,
3429
+ agentName: name,
3430
+ depth,
3431
+ data: event.data,
3432
+ timestamp: Date.now()
3433
+ };
3434
+ this.logEvent(orchestratorEvent);
3435
+ if (this.isJsonMode && this.jsonOutput) {
3436
+ this.emitAgentEventAsJSON(event, name, depth);
3437
+ }
3438
+ });
3439
+ }
3440
+ /**
3441
+ * Emit an agent event as JSON.
3442
+ */
3443
+ emitAgentEventAsJSON(event, agentName, depth) {
3444
+ if (!this.jsonOutput) return;
3445
+ const data = event.data;
3446
+ switch (event.type) {
3447
+ case "agent_start":
3448
+ this.jsonOutput.emitAgentStart(
3449
+ agentName,
3450
+ depth,
3451
+ data.task,
3452
+ data.name ? "ToolUseAgent" : "CodeAgent",
3453
+ // Will be improved
3454
+ 20
3455
+ // Default maxSteps, could be passed
3456
+ );
3457
+ break;
3458
+ case "agent_step":
3459
+ this.jsonOutput.emitAgentStep(
3460
+ agentName,
3461
+ depth,
3462
+ data.step,
3463
+ data.maxSteps,
3464
+ "start"
3465
+ );
3466
+ break;
3467
+ case "agent_thinking":
3468
+ this.jsonOutput.emitAgentThinking(
3469
+ agentName,
3470
+ depth,
3471
+ data.step,
3472
+ data.content,
3473
+ false
3474
+ );
3475
+ break;
3476
+ case "agent_tool_call":
3477
+ this.jsonOutput.emitToolCall(
3478
+ agentName,
3479
+ depth,
3480
+ data.step,
3481
+ data.toolCallId,
3482
+ data.toolName,
3483
+ data.arguments
3484
+ );
3485
+ break;
3486
+ case "agent_tool_result":
3487
+ this.jsonOutput.emitToolResult(
3488
+ agentName,
3489
+ depth,
3490
+ data.step,
3491
+ data.toolCallId,
3492
+ data.toolName,
3493
+ data.result,
3494
+ data.error,
3495
+ data.duration
3496
+ );
3497
+ break;
3498
+ case "agent_observation":
3499
+ this.jsonOutput.emitObservation(
3500
+ agentName,
3501
+ depth,
3502
+ data.step,
3503
+ data.observation,
3504
+ data.codeAction,
3505
+ data.logs
3506
+ );
3507
+ break;
3508
+ case "agent_error":
3509
+ this.jsonOutput.emitError(
3510
+ data.error,
3511
+ void 0,
3512
+ agentName,
3513
+ depth,
3514
+ data.step
3515
+ );
3516
+ break;
3517
+ case "agent_end":
3518
+ this.jsonOutput.emitAgentEnd(
3519
+ agentName,
3520
+ depth,
3521
+ data.output,
3522
+ 0,
3523
+ // totalSteps - would need to track
3524
+ data.tokenUsage,
3525
+ data.duration,
3526
+ true
3527
+ );
3528
+ break;
3529
+ }
2982
3530
  }
2983
3531
  /**
2984
3532
  * Display workflow info at startup.
2985
3533
  */
2986
- displayWorkflowInfo(workflow) {
3534
+ displayWorkflowInfo(workflow, _sourcePath) {
3535
+ const agents = Array.from(workflow.agents.keys());
3536
+ const tools = Array.from(workflow.tools.keys());
3537
+ const entrypoint = workflow.entrypointAgent.getName();
3538
+ if (this.isJsonMode && this.jsonOutput) {
3539
+ this.jsonOutput.emitWorkflowLoaded(
3540
+ workflow.name,
3541
+ workflow.description,
3542
+ agents,
3543
+ tools,
3544
+ entrypoint
3545
+ );
3546
+ return;
3547
+ }
2987
3548
  if (!this.config.verbose) return;
2988
3549
  const line = "\u2550".repeat(70);
2989
3550
  console.log(chalk2.cyan(line));
@@ -2991,16 +3552,20 @@ var Orchestrator = class {
2991
3552
  if (workflow.description) {
2992
3553
  console.log(chalk2.cyan(` ${workflow.description}`));
2993
3554
  }
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()}`));
3555
+ console.log(chalk2.cyan(` Agents: ${agents.join(", ")}`));
3556
+ console.log(chalk2.cyan(` Tools: ${tools.join(", ") || "(none defined at workflow level)"}`));
3557
+ console.log(chalk2.cyan(` Entrypoint: ${entrypoint}`));
2997
3558
  console.log(chalk2.cyan(line));
2998
3559
  console.log();
2999
3560
  }
3000
3561
  /**
3001
3562
  * Display run start info.
3002
3563
  */
3003
- displayRunStart(workflowName, task) {
3564
+ displayRunStart(workflowName, task, workflowPath) {
3565
+ if (this.isJsonMode && this.jsonOutput) {
3566
+ this.jsonOutput.emitRunStart(workflowPath || workflowName, task, this.config.cwd);
3567
+ return;
3568
+ }
3004
3569
  if (!this.config.verbose) return;
3005
3570
  console.log(chalk2.green.bold(`
3006
3571
  \u25B6 Running workflow "${workflowName}"`));
@@ -3010,7 +3575,16 @@ var Orchestrator = class {
3010
3575
  /**
3011
3576
  * Display run completion info.
3012
3577
  */
3013
- displayRunEnd(result) {
3578
+ displayRunEnd(result, success = true) {
3579
+ if (this.isJsonMode && this.jsonOutput) {
3580
+ this.jsonOutput.emitRunEnd(
3581
+ success,
3582
+ result.output,
3583
+ result.tokenUsage.totalTokens,
3584
+ result.steps.length
3585
+ );
3586
+ return;
3587
+ }
3014
3588
  if (!this.config.verbose) return;
3015
3589
  console.log(chalk2.gray("\n" + "\u2500".repeat(70)));
3016
3590
  console.log(chalk2.green.bold(`
@@ -3028,6 +3602,10 @@ var Orchestrator = class {
3028
3602
  * Display an error.
3029
3603
  */
3030
3604
  displayError(error) {
3605
+ if (this.isJsonMode && this.jsonOutput) {
3606
+ this.jsonOutput.emitError(error.message, error.stack);
3607
+ return;
3608
+ }
3031
3609
  if (!this.config.verbose) return;
3032
3610
  console.error(chalk2.red.bold(`
3033
3611
  \u274C Workflow failed: ${error.message}`));
@@ -3056,6 +3634,24 @@ var Orchestrator = class {
3056
3634
  getLoader() {
3057
3635
  return this.loader;
3058
3636
  }
3637
+ /**
3638
+ * Get the JSON output handler (if in JSON mode).
3639
+ */
3640
+ getJSONOutputHandler() {
3641
+ return this.jsonOutput;
3642
+ }
3643
+ /**
3644
+ * Check if in JSON output mode.
3645
+ */
3646
+ isJSONOutputMode() {
3647
+ return this.isJsonMode;
3648
+ }
3649
+ /**
3650
+ * Get the run ID.
3651
+ */
3652
+ getRunId() {
3653
+ return this.config.runId;
3654
+ }
3059
3655
  };
3060
3656
  export {
3061
3657
  Agent,
@@ -3069,11 +3665,13 @@ export {
3069
3665
  ExaSearchTool,
3070
3666
  FINAL_ANSWER_PROMPT,
3071
3667
  FinalAnswerTool,
3668
+ JSONOutputHandler,
3072
3669
  LocalExecutor,
3073
3670
  LogLevel,
3074
3671
  Model,
3075
3672
  OpenAIModel,
3076
3673
  Orchestrator,
3674
+ ProxyTool,
3077
3675
  ReadFileTool,
3078
3676
  Tool,
3079
3677
  ToolUseAgent,
@@ -3086,6 +3684,8 @@ export {
3086
3684
  formatToolDescriptions,
3087
3685
  generateSystemPrompt,
3088
3686
  generateToolUseSystemPrompt,
3089
- getErrorRecoveryPrompt
3687
+ getErrorRecoveryPrompt,
3688
+ loadCustomTools,
3689
+ scanCustomTools
3090
3690
  };
3091
3691
  //# sourceMappingURL=index.mjs.map