@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/README.md +407 -154
- package/dist/cli.js +826 -133
- package/dist/cli.js.map +1 -1
- package/dist/index.d.mts +320 -9
- package/dist/index.d.ts +320 -9
- package/dist/index.js +727 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +722 -122
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var fs8 = __toESM(require("fs"));
|
|
28
|
+
var path8 = __toESM(require("path"));
|
|
29
29
|
var readline = __toESM(require("readline"));
|
|
30
30
|
var import_chalk3 = __toESM(require("chalk"));
|
|
31
31
|
var import_dotenv = __toESM(require("dotenv"));
|
|
@@ -741,9 +741,21 @@ Total time: ${(duration / 1e3).toFixed(2)}s`);
|
|
|
741
741
|
getName() {
|
|
742
742
|
return this.config.name;
|
|
743
743
|
}
|
|
744
|
+
/** Get agent type identifier */
|
|
745
|
+
getType() {
|
|
746
|
+
return this.constructor.name;
|
|
747
|
+
}
|
|
748
|
+
/** Get max steps configuration */
|
|
749
|
+
getMaxSteps() {
|
|
750
|
+
return this.config.maxSteps;
|
|
751
|
+
}
|
|
752
|
+
/** Set the event callback (useful for orchestration) */
|
|
753
|
+
setOnEvent(callback) {
|
|
754
|
+
this.config.onEvent = callback;
|
|
755
|
+
}
|
|
744
756
|
/** Sleep for a specified duration */
|
|
745
757
|
sleep(ms) {
|
|
746
|
-
return new Promise((
|
|
758
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
747
759
|
}
|
|
748
760
|
};
|
|
749
761
|
|
|
@@ -1846,6 +1858,10 @@ Please try a different approach.`
|
|
|
1846
1858
|
memoryStep.tokenUsage = response.tokenUsage;
|
|
1847
1859
|
if (response.content && response.content.trim()) {
|
|
1848
1860
|
this.logger.reasoning(response.content.trim());
|
|
1861
|
+
this.emitEvent("agent_thinking", {
|
|
1862
|
+
step: this.currentStep,
|
|
1863
|
+
content: response.content.trim()
|
|
1864
|
+
});
|
|
1849
1865
|
}
|
|
1850
1866
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1851
1867
|
this.logger.warn("No tool calls in response. Prompting model to use tools.");
|
|
@@ -1878,42 +1894,84 @@ Please try a different approach.`
|
|
|
1878
1894
|
async processToolCalls(toolCalls) {
|
|
1879
1895
|
const results = [];
|
|
1880
1896
|
const executeTool = async (tc) => {
|
|
1897
|
+
const startTime = Date.now();
|
|
1881
1898
|
const toolName = tc.function.name;
|
|
1882
1899
|
const tool = this.tools.get(toolName);
|
|
1883
1900
|
if (!tool) {
|
|
1901
|
+
const error = `Unknown tool: ${toolName}. Available tools: ${Array.from(this.tools.keys()).join(", ")}`;
|
|
1902
|
+
this.emitEvent("agent_tool_result", {
|
|
1903
|
+
step: this.currentStep,
|
|
1904
|
+
toolCallId: tc.id,
|
|
1905
|
+
toolName,
|
|
1906
|
+
result: null,
|
|
1907
|
+
error,
|
|
1908
|
+
duration: Date.now() - startTime
|
|
1909
|
+
});
|
|
1884
1910
|
return {
|
|
1885
1911
|
toolCallId: tc.id,
|
|
1886
1912
|
toolName,
|
|
1887
1913
|
result: null,
|
|
1888
|
-
error
|
|
1914
|
+
error
|
|
1889
1915
|
};
|
|
1890
1916
|
}
|
|
1891
1917
|
let args;
|
|
1892
1918
|
try {
|
|
1893
1919
|
args = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
|
|
1894
1920
|
} catch {
|
|
1921
|
+
const error = `Failed to parse tool arguments: ${tc.function.arguments}`;
|
|
1922
|
+
this.emitEvent("agent_tool_result", {
|
|
1923
|
+
step: this.currentStep,
|
|
1924
|
+
toolCallId: tc.id,
|
|
1925
|
+
toolName,
|
|
1926
|
+
result: null,
|
|
1927
|
+
error,
|
|
1928
|
+
duration: Date.now() - startTime
|
|
1929
|
+
});
|
|
1895
1930
|
return {
|
|
1896
1931
|
toolCallId: tc.id,
|
|
1897
1932
|
toolName,
|
|
1898
1933
|
result: null,
|
|
1899
|
-
error
|
|
1934
|
+
error
|
|
1900
1935
|
};
|
|
1901
1936
|
}
|
|
1902
1937
|
this.logger.info(` Calling tool: ${toolName}(${JSON.stringify(args).slice(0, 100)}...)`);
|
|
1903
|
-
this.emitEvent("agent_tool_call", {
|
|
1938
|
+
this.emitEvent("agent_tool_call", {
|
|
1939
|
+
step: this.currentStep,
|
|
1940
|
+
toolCallId: tc.id,
|
|
1941
|
+
toolName,
|
|
1942
|
+
arguments: args
|
|
1943
|
+
});
|
|
1904
1944
|
try {
|
|
1905
1945
|
const result = await tool.call(args);
|
|
1946
|
+
const duration = Date.now() - startTime;
|
|
1947
|
+
this.emitEvent("agent_tool_result", {
|
|
1948
|
+
step: this.currentStep,
|
|
1949
|
+
toolCallId: tc.id,
|
|
1950
|
+
toolName,
|
|
1951
|
+
result,
|
|
1952
|
+
duration
|
|
1953
|
+
});
|
|
1906
1954
|
return {
|
|
1907
1955
|
toolCallId: tc.id,
|
|
1908
1956
|
toolName,
|
|
1909
1957
|
result
|
|
1910
1958
|
};
|
|
1911
1959
|
} catch (error) {
|
|
1960
|
+
const errorMsg = `Tool execution error: ${error.message}`;
|
|
1961
|
+
const duration = Date.now() - startTime;
|
|
1962
|
+
this.emitEvent("agent_tool_result", {
|
|
1963
|
+
step: this.currentStep,
|
|
1964
|
+
toolCallId: tc.id,
|
|
1965
|
+
toolName,
|
|
1966
|
+
result: null,
|
|
1967
|
+
error: errorMsg,
|
|
1968
|
+
duration
|
|
1969
|
+
});
|
|
1912
1970
|
return {
|
|
1913
1971
|
toolCallId: tc.id,
|
|
1914
1972
|
toolName,
|
|
1915
1973
|
result: null,
|
|
1916
|
-
error:
|
|
1974
|
+
error: errorMsg
|
|
1917
1975
|
};
|
|
1918
1976
|
}
|
|
1919
1977
|
};
|
|
@@ -2036,7 +2094,7 @@ async function ${this.name}(task: string): Promise<string> { ... }
|
|
|
2036
2094
|
};
|
|
2037
2095
|
|
|
2038
2096
|
// src/models/OpenAIModel.ts
|
|
2039
|
-
var import_openai = __toESM(require("openai"));
|
|
2097
|
+
var import_openai = __toESM(require("openai/index.mjs"));
|
|
2040
2098
|
|
|
2041
2099
|
// src/models/Model.ts
|
|
2042
2100
|
var Model = class {
|
|
@@ -2579,40 +2637,35 @@ var ExaGetContentsTool = class extends Tool {
|
|
|
2579
2637
|
// src/tools/ExaResearchTool.ts
|
|
2580
2638
|
var ExaResearchTool = class extends Tool {
|
|
2581
2639
|
name = "exa_research";
|
|
2582
|
-
description = "Perform
|
|
2640
|
+
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.";
|
|
2583
2641
|
inputs = {
|
|
2584
|
-
|
|
2642
|
+
instructions: {
|
|
2585
2643
|
type: "string",
|
|
2586
|
-
description: "
|
|
2644
|
+
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.",
|
|
2587
2645
|
required: true
|
|
2588
2646
|
},
|
|
2589
|
-
|
|
2590
|
-
type: "number",
|
|
2591
|
-
description: "Number of primary sources to retrieve (default: 5, max: 10)",
|
|
2592
|
-
required: false,
|
|
2593
|
-
default: 5
|
|
2594
|
-
},
|
|
2595
|
-
category: {
|
|
2647
|
+
model: {
|
|
2596
2648
|
type: "string",
|
|
2597
|
-
description: '
|
|
2598
|
-
required: false
|
|
2599
|
-
},
|
|
2600
|
-
includeDomains: {
|
|
2601
|
-
type: "array",
|
|
2602
|
-
description: "Only include results from these domains",
|
|
2649
|
+
description: 'Research model: "exa-research-fast" (faster, cheaper), "exa-research" (balanced, default), or "exa-research-pro" (most thorough)',
|
|
2603
2650
|
required: false
|
|
2604
2651
|
},
|
|
2605
|
-
|
|
2606
|
-
type: "
|
|
2607
|
-
description: "
|
|
2652
|
+
outputSchema: {
|
|
2653
|
+
type: "object",
|
|
2654
|
+
description: "Optional JSON Schema to enforce structured output format (max 8 root fields, 5 levels deep). If omitted, returns markdown report.",
|
|
2608
2655
|
required: false
|
|
2609
2656
|
}
|
|
2610
2657
|
};
|
|
2611
2658
|
outputType = "string";
|
|
2612
2659
|
apiKey;
|
|
2660
|
+
defaultModel;
|
|
2661
|
+
pollInterval;
|
|
2662
|
+
maxPollTime;
|
|
2613
2663
|
constructor(config) {
|
|
2614
2664
|
super();
|
|
2615
2665
|
this.apiKey = config?.apiKey ?? process.env.EXA_API_KEY ?? "";
|
|
2666
|
+
this.defaultModel = config?.model ?? "exa-research";
|
|
2667
|
+
this.pollInterval = config?.pollInterval ?? 2e3;
|
|
2668
|
+
this.maxPollTime = config?.maxPollTime ?? 3e5;
|
|
2616
2669
|
}
|
|
2617
2670
|
async setup() {
|
|
2618
2671
|
if (!this.apiKey) {
|
|
@@ -2621,91 +2674,87 @@ var ExaResearchTool = class extends Tool {
|
|
|
2621
2674
|
this.isSetup = true;
|
|
2622
2675
|
}
|
|
2623
2676
|
async execute(args) {
|
|
2624
|
-
const
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2677
|
+
const instructions = args.instructions;
|
|
2678
|
+
const model = args.model ?? this.defaultModel;
|
|
2679
|
+
const outputSchema = args.outputSchema;
|
|
2680
|
+
if (!instructions || instructions.length > 4096) {
|
|
2681
|
+
throw new Error("Instructions are required and must be <= 4096 characters");
|
|
2682
|
+
}
|
|
2683
|
+
const createBody = {
|
|
2684
|
+
instructions,
|
|
2685
|
+
model
|
|
2631
2686
|
};
|
|
2632
|
-
if (
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
const
|
|
2687
|
+
if (outputSchema) {
|
|
2688
|
+
createBody.outputSchema = outputSchema;
|
|
2689
|
+
}
|
|
2690
|
+
const createResponse = await fetch("https://api.exa.ai/research/v1", {
|
|
2636
2691
|
method: "POST",
|
|
2637
2692
|
headers: {
|
|
2638
2693
|
"x-api-key": this.apiKey,
|
|
2639
2694
|
"Content-Type": "application/json"
|
|
2640
2695
|
},
|
|
2641
|
-
body: JSON.stringify(
|
|
2696
|
+
body: JSON.stringify(createBody)
|
|
2642
2697
|
});
|
|
2643
|
-
if (!
|
|
2644
|
-
const errorText = await
|
|
2645
|
-
throw new Error(`
|
|
2646
|
-
}
|
|
2647
|
-
const searchData = await searchResponse.json();
|
|
2648
|
-
if (!searchData.results || searchData.results.length === 0) {
|
|
2649
|
-
return `No research sources found for topic: "${topic}"`;
|
|
2698
|
+
if (!createResponse.ok) {
|
|
2699
|
+
const errorText = await createResponse.text();
|
|
2700
|
+
throw new Error(`Failed to create research task (${createResponse.status}): ${errorText}`);
|
|
2650
2701
|
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
body: JSON.stringify(similarBody)
|
|
2666
|
-
});
|
|
2667
|
-
if (similarResponse.ok) {
|
|
2668
|
-
const similarData = await similarResponse.json();
|
|
2669
|
-
similarResults = similarData.results ?? [];
|
|
2702
|
+
const createData = await createResponse.json();
|
|
2703
|
+
const researchId = createData.researchId;
|
|
2704
|
+
console.log(`Research task created: ${researchId}. Polling for results...`);
|
|
2705
|
+
const startTime = Date.now();
|
|
2706
|
+
let attempts = 0;
|
|
2707
|
+
while (Date.now() - startTime < this.maxPollTime) {
|
|
2708
|
+
attempts++;
|
|
2709
|
+
if (attempts > 1) {
|
|
2710
|
+
await new Promise((resolve7) => setTimeout(resolve7, this.pollInterval));
|
|
2711
|
+
}
|
|
2712
|
+
const statusResponse = await fetch(`https://api.exa.ai/research/v1/${researchId}`, {
|
|
2713
|
+
method: "GET",
|
|
2714
|
+
headers: {
|
|
2715
|
+
"x-api-key": this.apiKey
|
|
2670
2716
|
}
|
|
2671
|
-
}
|
|
2717
|
+
});
|
|
2718
|
+
if (!statusResponse.ok) {
|
|
2719
|
+
const errorText = await statusResponse.text();
|
|
2720
|
+
throw new Error(`Failed to check research status (${statusResponse.status}): ${errorText}`);
|
|
2672
2721
|
}
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2722
|
+
const statusData = await statusResponse.json();
|
|
2723
|
+
if (statusData.status === "completed") {
|
|
2724
|
+
const output = statusData.output;
|
|
2725
|
+
if (!output) {
|
|
2726
|
+
throw new Error("Research completed but no output was returned");
|
|
2727
|
+
}
|
|
2728
|
+
const sections = [];
|
|
2729
|
+
if (outputSchema && output.parsed) {
|
|
2730
|
+
sections.push("# Research Results (Structured)\n");
|
|
2731
|
+
sections.push(JSON.stringify(output.parsed, null, 2));
|
|
2732
|
+
} else {
|
|
2733
|
+
sections.push(output.content);
|
|
2734
|
+
}
|
|
2735
|
+
if (statusData.costDollars) {
|
|
2736
|
+
const cost = statusData.costDollars;
|
|
2737
|
+
sections.push("\n---\n");
|
|
2738
|
+
sections.push("## Research Metrics\n");
|
|
2739
|
+
sections.push(`- **Cost**: $${cost.total.toFixed(4)}`);
|
|
2740
|
+
sections.push(`- **Searches**: ${cost.numSearches}`);
|
|
2741
|
+
sections.push(`- **Pages analyzed**: ${cost.numPages}`);
|
|
2742
|
+
sections.push(`- **Reasoning tokens**: ${cost.reasoningTokens.toLocaleString()}`);
|
|
2743
|
+
}
|
|
2744
|
+
console.log(`Research completed in ${attempts} polls (${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
|
|
2745
|
+
return sections.join("\n");
|
|
2746
|
+
}
|
|
2747
|
+
if (statusData.status === "failed") {
|
|
2748
|
+
throw new Error(`Research failed: ${statusData.error ?? "Unknown error"}`);
|
|
2693
2749
|
}
|
|
2694
|
-
if ("
|
|
2695
|
-
|
|
2750
|
+
if (statusData.status === "canceled") {
|
|
2751
|
+
throw new Error("Research was canceled");
|
|
2696
2752
|
}
|
|
2697
|
-
if (
|
|
2698
|
-
|
|
2699
|
-
Content:
|
|
2700
|
-
${source.text.slice(0, 2e3)}`);
|
|
2753
|
+
if (attempts % 10 === 0) {
|
|
2754
|
+
console.log(`Still researching... (${attempts} polls, ${((Date.now() - startTime) / 1e3).toFixed(0)}s elapsed)`);
|
|
2701
2755
|
}
|
|
2702
|
-
sections.push("");
|
|
2703
2756
|
}
|
|
2704
|
-
|
|
2705
|
-
uniqueSources.forEach((s, i) => {
|
|
2706
|
-
sections.push(`${i + 1}. ${s.url}`);
|
|
2707
|
-
});
|
|
2708
|
-
return sections.join("\n");
|
|
2757
|
+
throw new Error(`Research timed out after ${this.maxPollTime / 1e3}s. Task ID: ${researchId}`);
|
|
2709
2758
|
}
|
|
2710
2759
|
};
|
|
2711
2760
|
|
|
@@ -2721,12 +2770,21 @@ var TOOL_REGISTRY = {
|
|
|
2721
2770
|
};
|
|
2722
2771
|
var YAMLLoader = class {
|
|
2723
2772
|
customTools = /* @__PURE__ */ new Map();
|
|
2773
|
+
toolInstances = /* @__PURE__ */ new Map();
|
|
2724
2774
|
/**
|
|
2725
|
-
* Register a custom tool type for use in YAML definitions.
|
|
2775
|
+
* Register a custom tool type (class) for use in YAML definitions.
|
|
2726
2776
|
*/
|
|
2727
2777
|
registerToolType(typeName, toolClass) {
|
|
2728
2778
|
this.customTools.set(typeName, toolClass);
|
|
2729
2779
|
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Register a pre-built tool instance for use in YAML definitions.
|
|
2782
|
+
* Used by the custom tool system to register ProxyTool instances
|
|
2783
|
+
* that are created by the scanner rather than by class instantiation.
|
|
2784
|
+
*/
|
|
2785
|
+
registerToolInstance(typeName, tool) {
|
|
2786
|
+
this.toolInstances.set(typeName, tool);
|
|
2787
|
+
}
|
|
2730
2788
|
/**
|
|
2731
2789
|
* Load a workflow from a YAML file path.
|
|
2732
2790
|
*/
|
|
@@ -2810,9 +2868,16 @@ var YAMLLoader = class {
|
|
|
2810
2868
|
* Build a tool instance from a type name and config.
|
|
2811
2869
|
*/
|
|
2812
2870
|
buildTool(name, type, config) {
|
|
2871
|
+
const existingInstance = this.toolInstances.get(type);
|
|
2872
|
+
if (existingInstance) {
|
|
2873
|
+
if (name !== type && name !== existingInstance.name) {
|
|
2874
|
+
Object.defineProperty(existingInstance, "name", { value: name, writable: false });
|
|
2875
|
+
}
|
|
2876
|
+
return existingInstance;
|
|
2877
|
+
}
|
|
2813
2878
|
const ToolClass = TOOL_REGISTRY[type] ?? this.customTools.get(type);
|
|
2814
2879
|
if (!ToolClass) {
|
|
2815
|
-
throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys()].join(", ")}`);
|
|
2880
|
+
throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys(), ...this.toolInstances.keys()].join(", ")}`);
|
|
2816
2881
|
}
|
|
2817
2882
|
const tool = new ToolClass(config);
|
|
2818
2883
|
if (name !== type && name !== tool.name) {
|
|
@@ -2840,11 +2905,16 @@ var YAMLLoader = class {
|
|
|
2840
2905
|
if (tool) {
|
|
2841
2906
|
agentTools.push(tool);
|
|
2842
2907
|
} else {
|
|
2843
|
-
const
|
|
2844
|
-
if (
|
|
2845
|
-
agentTools.push(
|
|
2908
|
+
const instance = this.toolInstances.get(toolName);
|
|
2909
|
+
if (instance) {
|
|
2910
|
+
agentTools.push(instance);
|
|
2846
2911
|
} else {
|
|
2847
|
-
|
|
2912
|
+
const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
|
|
2913
|
+
if (ToolClass) {
|
|
2914
|
+
agentTools.push(new ToolClass());
|
|
2915
|
+
} else {
|
|
2916
|
+
throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
|
|
2917
|
+
}
|
|
2848
2918
|
}
|
|
2849
2919
|
}
|
|
2850
2920
|
}
|
|
@@ -2893,40 +2963,116 @@ var YAMLLoader = class {
|
|
|
2893
2963
|
}
|
|
2894
2964
|
};
|
|
2895
2965
|
|
|
2966
|
+
// src/output/JSONOutputHandler.ts
|
|
2967
|
+
var JSONOutputHandler = class {
|
|
2968
|
+
runId;
|
|
2969
|
+
startTime;
|
|
2970
|
+
constructor(config) {
|
|
2971
|
+
this.runId = config.runId;
|
|
2972
|
+
this.startTime = Date.now();
|
|
2973
|
+
}
|
|
2974
|
+
emit(type, data, extra = {}) {
|
|
2975
|
+
console.log(JSON.stringify({
|
|
2976
|
+
runId: this.runId,
|
|
2977
|
+
timestamp: Date.now(),
|
|
2978
|
+
type,
|
|
2979
|
+
...extra,
|
|
2980
|
+
data
|
|
2981
|
+
}));
|
|
2982
|
+
}
|
|
2983
|
+
emitRunStart(workflowPath, task, cwd) {
|
|
2984
|
+
this.emit("run_start", { workflowPath, task, cwd });
|
|
2985
|
+
}
|
|
2986
|
+
emitWorkflowLoaded(name, description, agents, tools, entrypoint) {
|
|
2987
|
+
this.emit("workflow_loaded", { name, description, agents, tools, entrypoint });
|
|
2988
|
+
}
|
|
2989
|
+
emitRunEnd(success, output, totalTokens, totalSteps) {
|
|
2990
|
+
this.emit("run_end", {
|
|
2991
|
+
success,
|
|
2992
|
+
output,
|
|
2993
|
+
totalDuration: Date.now() - this.startTime,
|
|
2994
|
+
totalTokens,
|
|
2995
|
+
totalSteps
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
emitAgentStart(agentName, depth, task, agentType, maxSteps) {
|
|
2999
|
+
this.emit("agent_start", { task, agentType, maxSteps }, { agentName, depth });
|
|
3000
|
+
}
|
|
3001
|
+
emitAgentEnd(agentName, depth, output, totalSteps, tokenUsage, duration, success) {
|
|
3002
|
+
this.emit("agent_end", { output, totalSteps, tokenUsage, duration, success }, { agentName, depth });
|
|
3003
|
+
}
|
|
3004
|
+
emitAgentStep(agentName, depth, stepNumber, maxSteps, phase) {
|
|
3005
|
+
this.emit("agent_step", { stepNumber, maxSteps, phase }, { agentName, depth });
|
|
3006
|
+
}
|
|
3007
|
+
emitAgentThinking(agentName, depth, stepNumber, content, isPartial) {
|
|
3008
|
+
this.emit("agent_thinking", { stepNumber, content, isPartial }, { agentName, depth });
|
|
3009
|
+
}
|
|
3010
|
+
emitToolCall(agentName, depth, stepNumber, toolCallId, toolName, args) {
|
|
3011
|
+
this.emit("agent_tool_call", { stepNumber, toolCallId, toolName, arguments: args }, { agentName, depth });
|
|
3012
|
+
}
|
|
3013
|
+
emitToolResult(agentName, depth, stepNumber, toolCallId, toolName, result, error, duration) {
|
|
3014
|
+
this.emit("agent_tool_result", { stepNumber, toolCallId, toolName, result, error, duration }, { agentName, depth });
|
|
3015
|
+
}
|
|
3016
|
+
emitObservation(agentName, depth, stepNumber, observation, codeAction, logs) {
|
|
3017
|
+
this.emit("agent_observation", { stepNumber, observation, codeAction, logs }, { agentName, depth });
|
|
3018
|
+
}
|
|
3019
|
+
emitError(message, stack, agentName, depth, stepNumber) {
|
|
3020
|
+
this.emit("error", { message, stack, stepNumber }, {
|
|
3021
|
+
...agentName ? { agentName } : {},
|
|
3022
|
+
...depth !== void 0 ? { depth } : {}
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
|
|
2896
3027
|
// src/orchestrator/Orchestrator.ts
|
|
2897
3028
|
var Orchestrator = class {
|
|
2898
3029
|
loader;
|
|
2899
3030
|
config;
|
|
2900
3031
|
activeAgents = /* @__PURE__ */ new Map();
|
|
2901
3032
|
eventLog = [];
|
|
3033
|
+
jsonOutput = null;
|
|
3034
|
+
isJsonMode = false;
|
|
2902
3035
|
constructor(config = {}) {
|
|
2903
3036
|
this.loader = new YAMLLoader();
|
|
3037
|
+
this.isJsonMode = config.outputFormat === "json";
|
|
2904
3038
|
this.config = {
|
|
2905
3039
|
verbose: config.verbose ?? true,
|
|
2906
|
-
onEvent: config.onEvent
|
|
3040
|
+
onEvent: config.onEvent,
|
|
3041
|
+
outputFormat: config.outputFormat ?? "text",
|
|
3042
|
+
runId: config.runId,
|
|
3043
|
+
cwd: config.cwd
|
|
2907
3044
|
};
|
|
3045
|
+
if (this.isJsonMode) {
|
|
3046
|
+
if (!config.runId) {
|
|
3047
|
+
throw new Error("runId is required for JSON output mode");
|
|
3048
|
+
}
|
|
3049
|
+
this.jsonOutput = new JSONOutputHandler({
|
|
3050
|
+
runId: config.runId,
|
|
3051
|
+
verbose: config.verbose ?? true
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
2908
3054
|
}
|
|
2909
3055
|
/**
|
|
2910
3056
|
* Load a workflow from a YAML file.
|
|
2911
3057
|
*/
|
|
2912
3058
|
loadWorkflow(filePath) {
|
|
2913
3059
|
const workflow = this.loader.loadFromFile(filePath);
|
|
2914
|
-
this.displayWorkflowInfo(workflow);
|
|
3060
|
+
this.displayWorkflowInfo(workflow, filePath);
|
|
2915
3061
|
return workflow;
|
|
2916
3062
|
}
|
|
2917
3063
|
/**
|
|
2918
3064
|
* Load a workflow from YAML string.
|
|
2919
3065
|
*/
|
|
2920
|
-
loadWorkflowFromString(yamlContent) {
|
|
3066
|
+
loadWorkflowFromString(yamlContent, sourcePath) {
|
|
2921
3067
|
const workflow = this.loader.loadFromString(yamlContent);
|
|
2922
|
-
this.displayWorkflowInfo(workflow);
|
|
3068
|
+
this.displayWorkflowInfo(workflow, sourcePath);
|
|
2923
3069
|
return workflow;
|
|
2924
3070
|
}
|
|
2925
3071
|
/**
|
|
2926
3072
|
* Run a loaded workflow with a task.
|
|
2927
3073
|
*/
|
|
2928
|
-
async runWorkflow(workflow, task) {
|
|
2929
|
-
this.displayRunStart(workflow.name, task);
|
|
3074
|
+
async runWorkflow(workflow, task, workflowPath) {
|
|
3075
|
+
this.displayRunStart(workflow.name, task, workflowPath);
|
|
2930
3076
|
this.instrumentAgent(workflow.entrypointAgent, workflow.entrypointAgent.getName(), 0);
|
|
2931
3077
|
for (const [name, agent] of workflow.agents) {
|
|
2932
3078
|
if (agent !== workflow.entrypointAgent) {
|
|
@@ -2935,10 +3081,13 @@ var Orchestrator = class {
|
|
|
2935
3081
|
}
|
|
2936
3082
|
try {
|
|
2937
3083
|
const result = await workflow.entrypointAgent.run(task);
|
|
2938
|
-
this.displayRunEnd(result);
|
|
3084
|
+
this.displayRunEnd(result, true);
|
|
2939
3085
|
return result;
|
|
2940
3086
|
} catch (error) {
|
|
2941
3087
|
this.displayError(error);
|
|
3088
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3089
|
+
this.jsonOutput.emitRunEnd(false, null, 0, 0);
|
|
3090
|
+
}
|
|
2942
3091
|
throw error;
|
|
2943
3092
|
}
|
|
2944
3093
|
}
|
|
@@ -2955,11 +3104,128 @@ var Orchestrator = class {
|
|
|
2955
3104
|
*/
|
|
2956
3105
|
instrumentAgent(agent, name, depth) {
|
|
2957
3106
|
this.activeAgents.set(name, { agent, depth });
|
|
3107
|
+
agent.setOnEvent((event) => {
|
|
3108
|
+
const orchestratorEvent = {
|
|
3109
|
+
type: event.type,
|
|
3110
|
+
agentName: name,
|
|
3111
|
+
depth,
|
|
3112
|
+
data: event.data,
|
|
3113
|
+
timestamp: Date.now()
|
|
3114
|
+
};
|
|
3115
|
+
this.logEvent(orchestratorEvent);
|
|
3116
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3117
|
+
this.emitAgentEventAsJSON(event, name, depth);
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Emit an agent event as JSON.
|
|
3123
|
+
*/
|
|
3124
|
+
emitAgentEventAsJSON(event, agentName, depth) {
|
|
3125
|
+
if (!this.jsonOutput) return;
|
|
3126
|
+
const data = event.data;
|
|
3127
|
+
switch (event.type) {
|
|
3128
|
+
case "agent_start":
|
|
3129
|
+
this.jsonOutput.emitAgentStart(
|
|
3130
|
+
agentName,
|
|
3131
|
+
depth,
|
|
3132
|
+
data.task,
|
|
3133
|
+
data.name ? "ToolUseAgent" : "CodeAgent",
|
|
3134
|
+
// Will be improved
|
|
3135
|
+
20
|
|
3136
|
+
// Default maxSteps, could be passed
|
|
3137
|
+
);
|
|
3138
|
+
break;
|
|
3139
|
+
case "agent_step":
|
|
3140
|
+
this.jsonOutput.emitAgentStep(
|
|
3141
|
+
agentName,
|
|
3142
|
+
depth,
|
|
3143
|
+
data.step,
|
|
3144
|
+
data.maxSteps,
|
|
3145
|
+
"start"
|
|
3146
|
+
);
|
|
3147
|
+
break;
|
|
3148
|
+
case "agent_thinking":
|
|
3149
|
+
this.jsonOutput.emitAgentThinking(
|
|
3150
|
+
agentName,
|
|
3151
|
+
depth,
|
|
3152
|
+
data.step,
|
|
3153
|
+
data.content,
|
|
3154
|
+
false
|
|
3155
|
+
);
|
|
3156
|
+
break;
|
|
3157
|
+
case "agent_tool_call":
|
|
3158
|
+
this.jsonOutput.emitToolCall(
|
|
3159
|
+
agentName,
|
|
3160
|
+
depth,
|
|
3161
|
+
data.step,
|
|
3162
|
+
data.toolCallId,
|
|
3163
|
+
data.toolName,
|
|
3164
|
+
data.arguments
|
|
3165
|
+
);
|
|
3166
|
+
break;
|
|
3167
|
+
case "agent_tool_result":
|
|
3168
|
+
this.jsonOutput.emitToolResult(
|
|
3169
|
+
agentName,
|
|
3170
|
+
depth,
|
|
3171
|
+
data.step,
|
|
3172
|
+
data.toolCallId,
|
|
3173
|
+
data.toolName,
|
|
3174
|
+
data.result,
|
|
3175
|
+
data.error,
|
|
3176
|
+
data.duration
|
|
3177
|
+
);
|
|
3178
|
+
break;
|
|
3179
|
+
case "agent_observation":
|
|
3180
|
+
this.jsonOutput.emitObservation(
|
|
3181
|
+
agentName,
|
|
3182
|
+
depth,
|
|
3183
|
+
data.step,
|
|
3184
|
+
data.observation,
|
|
3185
|
+
data.codeAction,
|
|
3186
|
+
data.logs
|
|
3187
|
+
);
|
|
3188
|
+
break;
|
|
3189
|
+
case "agent_error":
|
|
3190
|
+
this.jsonOutput.emitError(
|
|
3191
|
+
data.error,
|
|
3192
|
+
void 0,
|
|
3193
|
+
agentName,
|
|
3194
|
+
depth,
|
|
3195
|
+
data.step
|
|
3196
|
+
);
|
|
3197
|
+
break;
|
|
3198
|
+
case "agent_end":
|
|
3199
|
+
this.jsonOutput.emitAgentEnd(
|
|
3200
|
+
agentName,
|
|
3201
|
+
depth,
|
|
3202
|
+
data.output,
|
|
3203
|
+
0,
|
|
3204
|
+
// totalSteps - would need to track
|
|
3205
|
+
data.tokenUsage,
|
|
3206
|
+
data.duration,
|
|
3207
|
+
true
|
|
3208
|
+
);
|
|
3209
|
+
break;
|
|
3210
|
+
}
|
|
2958
3211
|
}
|
|
2959
3212
|
/**
|
|
2960
3213
|
* Display workflow info at startup.
|
|
2961
3214
|
*/
|
|
2962
|
-
displayWorkflowInfo(workflow) {
|
|
3215
|
+
displayWorkflowInfo(workflow, _sourcePath) {
|
|
3216
|
+
const agents = Array.from(workflow.agents.keys());
|
|
3217
|
+
const tools = Array.from(workflow.tools.keys());
|
|
3218
|
+
const entrypoint = workflow.entrypointAgent.getName();
|
|
3219
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3220
|
+
this.jsonOutput.emitWorkflowLoaded(
|
|
3221
|
+
workflow.name,
|
|
3222
|
+
workflow.description,
|
|
3223
|
+
agents,
|
|
3224
|
+
tools,
|
|
3225
|
+
entrypoint
|
|
3226
|
+
);
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
2963
3229
|
if (!this.config.verbose) return;
|
|
2964
3230
|
const line = "\u2550".repeat(70);
|
|
2965
3231
|
console.log(import_chalk2.default.cyan(line));
|
|
@@ -2967,16 +3233,20 @@ var Orchestrator = class {
|
|
|
2967
3233
|
if (workflow.description) {
|
|
2968
3234
|
console.log(import_chalk2.default.cyan(` ${workflow.description}`));
|
|
2969
3235
|
}
|
|
2970
|
-
console.log(import_chalk2.default.cyan(` Agents: ${
|
|
2971
|
-
console.log(import_chalk2.default.cyan(` Tools: ${
|
|
2972
|
-
console.log(import_chalk2.default.cyan(` Entrypoint: ${
|
|
3236
|
+
console.log(import_chalk2.default.cyan(` Agents: ${agents.join(", ")}`));
|
|
3237
|
+
console.log(import_chalk2.default.cyan(` Tools: ${tools.join(", ") || "(none defined at workflow level)"}`));
|
|
3238
|
+
console.log(import_chalk2.default.cyan(` Entrypoint: ${entrypoint}`));
|
|
2973
3239
|
console.log(import_chalk2.default.cyan(line));
|
|
2974
3240
|
console.log();
|
|
2975
3241
|
}
|
|
2976
3242
|
/**
|
|
2977
3243
|
* Display run start info.
|
|
2978
3244
|
*/
|
|
2979
|
-
displayRunStart(workflowName, task) {
|
|
3245
|
+
displayRunStart(workflowName, task, workflowPath) {
|
|
3246
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3247
|
+
this.jsonOutput.emitRunStart(workflowPath || workflowName, task, this.config.cwd);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
2980
3250
|
if (!this.config.verbose) return;
|
|
2981
3251
|
console.log(import_chalk2.default.green.bold(`
|
|
2982
3252
|
\u25B6 Running workflow "${workflowName}"`));
|
|
@@ -2986,7 +3256,16 @@ var Orchestrator = class {
|
|
|
2986
3256
|
/**
|
|
2987
3257
|
* Display run completion info.
|
|
2988
3258
|
*/
|
|
2989
|
-
displayRunEnd(result) {
|
|
3259
|
+
displayRunEnd(result, success = true) {
|
|
3260
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3261
|
+
this.jsonOutput.emitRunEnd(
|
|
3262
|
+
success,
|
|
3263
|
+
result.output,
|
|
3264
|
+
result.tokenUsage.totalTokens,
|
|
3265
|
+
result.steps.length
|
|
3266
|
+
);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
2990
3269
|
if (!this.config.verbose) return;
|
|
2991
3270
|
console.log(import_chalk2.default.gray("\n" + "\u2500".repeat(70)));
|
|
2992
3271
|
console.log(import_chalk2.default.green.bold(`
|
|
@@ -3004,6 +3283,10 @@ var Orchestrator = class {
|
|
|
3004
3283
|
* Display an error.
|
|
3005
3284
|
*/
|
|
3006
3285
|
displayError(error) {
|
|
3286
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3287
|
+
this.jsonOutput.emitError(error.message, error.stack);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3007
3290
|
if (!this.config.verbose) return;
|
|
3008
3291
|
console.error(import_chalk2.default.red.bold(`
|
|
3009
3292
|
\u274C Workflow failed: ${error.message}`));
|
|
@@ -3032,8 +3315,321 @@ var Orchestrator = class {
|
|
|
3032
3315
|
getLoader() {
|
|
3033
3316
|
return this.loader;
|
|
3034
3317
|
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Get the JSON output handler (if in JSON mode).
|
|
3320
|
+
*/
|
|
3321
|
+
getJSONOutputHandler() {
|
|
3322
|
+
return this.jsonOutput;
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Check if in JSON output mode.
|
|
3326
|
+
*/
|
|
3327
|
+
isJSONOutputMode() {
|
|
3328
|
+
return this.isJsonMode;
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Get the run ID.
|
|
3332
|
+
*/
|
|
3333
|
+
getRunId() {
|
|
3334
|
+
return this.config.runId;
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
|
|
3338
|
+
// src/tools/CustomToolScanner.ts
|
|
3339
|
+
var fs7 = __toESM(require("fs"));
|
|
3340
|
+
var path7 = __toESM(require("path"));
|
|
3341
|
+
|
|
3342
|
+
// src/tools/ProxyTool.ts
|
|
3343
|
+
var import_child_process2 = require("child_process");
|
|
3344
|
+
|
|
3345
|
+
// src/utils/bunInstaller.ts
|
|
3346
|
+
var import_child_process = require("child_process");
|
|
3347
|
+
var path6 = __toESM(require("path"));
|
|
3348
|
+
var fs6 = __toESM(require("fs"));
|
|
3349
|
+
var os3 = __toESM(require("os"));
|
|
3350
|
+
var cachedBunPath = null;
|
|
3351
|
+
async function ensureBunAvailable() {
|
|
3352
|
+
if (cachedBunPath) return cachedBunPath;
|
|
3353
|
+
const fromPath = whichBun();
|
|
3354
|
+
if (fromPath) {
|
|
3355
|
+
cachedBunPath = fromPath;
|
|
3356
|
+
return fromPath;
|
|
3357
|
+
}
|
|
3358
|
+
const localPath = path6.join(os3.homedir(), ".bun", "bin", "bun");
|
|
3359
|
+
if (fs6.existsSync(localPath)) {
|
|
3360
|
+
cachedBunPath = localPath;
|
|
3361
|
+
return localPath;
|
|
3362
|
+
}
|
|
3363
|
+
console.log(
|
|
3364
|
+
"\n[smol-js] Bun is required to run custom tools but was not found. Installing Bun automatically...\n"
|
|
3365
|
+
);
|
|
3366
|
+
try {
|
|
3367
|
+
(0, import_child_process.execSync)("curl --proto =https --tlsv1.2 -sSf https://bun.sh | bash", {
|
|
3368
|
+
stdio: "inherit",
|
|
3369
|
+
shell: "/bin/bash",
|
|
3370
|
+
env: { ...process.env, HOME: os3.homedir() }
|
|
3371
|
+
});
|
|
3372
|
+
} catch (err) {
|
|
3373
|
+
throw new Error(
|
|
3374
|
+
`[smol-js] Failed to auto-install Bun. Please install it manually: https://bun.sh
|
|
3375
|
+
Details: ${err.message}`
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
const afterInstall = whichBun() || (fs6.existsSync(localPath) ? localPath : null);
|
|
3379
|
+
if (!afterInstall) {
|
|
3380
|
+
throw new Error(
|
|
3381
|
+
"[smol-js] Bun installation appeared to succeed but the binary was not found. Please install manually: https://bun.sh"
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
console.log(`[smol-js] Bun installed successfully at: ${afterInstall}
|
|
3385
|
+
`);
|
|
3386
|
+
cachedBunPath = afterInstall;
|
|
3387
|
+
return afterInstall;
|
|
3388
|
+
}
|
|
3389
|
+
function whichBun() {
|
|
3390
|
+
try {
|
|
3391
|
+
const cmd = process.platform === "win32" ? "where bun" : "which bun";
|
|
3392
|
+
const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
|
|
3393
|
+
const first = result.split("\n")[0]?.trim();
|
|
3394
|
+
if (first && fs6.existsSync(first)) return first;
|
|
3395
|
+
return null;
|
|
3396
|
+
} catch {
|
|
3397
|
+
return null;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// src/tools/ProxyTool.ts
|
|
3402
|
+
var TOOL_OUTPUT_PREFIX = "[TOOL_OUTPUT]";
|
|
3403
|
+
var TOOL_RESULT_PREFIX = "[TOOL_RESULT]";
|
|
3404
|
+
var TOOL_ERROR_PREFIX = "[TOOL_ERROR]";
|
|
3405
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 6e4;
|
|
3406
|
+
var ProxyTool = class extends Tool {
|
|
3407
|
+
name;
|
|
3408
|
+
description;
|
|
3409
|
+
inputs;
|
|
3410
|
+
outputType;
|
|
3411
|
+
toolPath;
|
|
3412
|
+
timeout;
|
|
3413
|
+
bunPath = null;
|
|
3414
|
+
constructor(config) {
|
|
3415
|
+
super();
|
|
3416
|
+
this.name = config.name;
|
|
3417
|
+
this.description = config.description;
|
|
3418
|
+
this.inputs = config.inputs;
|
|
3419
|
+
this.outputType = config.outputType;
|
|
3420
|
+
this.toolPath = config.toolPath;
|
|
3421
|
+
this.timeout = config.timeout ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Ensure Bun is available before first invocation.
|
|
3425
|
+
*/
|
|
3426
|
+
async setup() {
|
|
3427
|
+
this.bunPath = await ensureBunAvailable();
|
|
3428
|
+
this.isSetup = true;
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Spawn the tool in a Bun child process, pass serialized args via CLI,
|
|
3432
|
+
* stream stdout back as log lines, and parse the final result.
|
|
3433
|
+
*/
|
|
3434
|
+
async execute(args) {
|
|
3435
|
+
if (!this.bunPath) {
|
|
3436
|
+
await this.setup();
|
|
3437
|
+
}
|
|
3438
|
+
const serializedArgs = JSON.stringify(args);
|
|
3439
|
+
return new Promise((resolve7, reject) => {
|
|
3440
|
+
const child = (0, import_child_process2.spawn)(this.bunPath, ["run", this.toolPath, serializedArgs], {
|
|
3441
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3442
|
+
env: { ...process.env }
|
|
3443
|
+
});
|
|
3444
|
+
let result = void 0;
|
|
3445
|
+
let resultReceived = false;
|
|
3446
|
+
let errorMessage = null;
|
|
3447
|
+
const logBuffer = [];
|
|
3448
|
+
let stderr = "";
|
|
3449
|
+
let partialLine = "";
|
|
3450
|
+
child.stdout.on("data", (chunk) => {
|
|
3451
|
+
partialLine += chunk.toString("utf8");
|
|
3452
|
+
const lines = partialLine.split("\n");
|
|
3453
|
+
partialLine = lines.pop();
|
|
3454
|
+
for (const line of lines) {
|
|
3455
|
+
this.processLine(line, {
|
|
3456
|
+
onOutput: (msg) => logBuffer.push(msg),
|
|
3457
|
+
onResult: (value) => {
|
|
3458
|
+
result = value;
|
|
3459
|
+
resultReceived = true;
|
|
3460
|
+
},
|
|
3461
|
+
onError: (msg) => {
|
|
3462
|
+
errorMessage = msg;
|
|
3463
|
+
}
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
});
|
|
3467
|
+
child.stderr.on("data", (chunk) => {
|
|
3468
|
+
stderr += chunk.toString("utf8");
|
|
3469
|
+
});
|
|
3470
|
+
const timer = setTimeout(() => {
|
|
3471
|
+
child.kill("SIGTERM");
|
|
3472
|
+
reject(new Error(
|
|
3473
|
+
`Custom tool "${this.name}" timed out after ${this.timeout}ms. The process was terminated. Check the tool for infinite loops or slow operations.`
|
|
3474
|
+
));
|
|
3475
|
+
}, this.timeout);
|
|
3476
|
+
timer.unref();
|
|
3477
|
+
child.on("close", (code) => {
|
|
3478
|
+
clearTimeout(timer);
|
|
3479
|
+
if (partialLine.trim()) {
|
|
3480
|
+
this.processLine(partialLine, {
|
|
3481
|
+
onOutput: (msg) => logBuffer.push(msg),
|
|
3482
|
+
onResult: (value) => {
|
|
3483
|
+
result = value;
|
|
3484
|
+
resultReceived = true;
|
|
3485
|
+
},
|
|
3486
|
+
onError: (msg) => {
|
|
3487
|
+
errorMessage = msg;
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
if (errorMessage) {
|
|
3492
|
+
reject(new Error(
|
|
3493
|
+
`Custom tool "${this.name}" reported an error: ${errorMessage}`
|
|
3494
|
+
));
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
if (resultReceived) {
|
|
3498
|
+
if (logBuffer.length > 0) {
|
|
3499
|
+
const logPrefix = `[Tool output logs]
|
|
3500
|
+
${logBuffer.join("\n")}
|
|
3501
|
+
|
|
3502
|
+
[Tool result]
|
|
3503
|
+
`;
|
|
3504
|
+
if (typeof result === "string") {
|
|
3505
|
+
resolve7(logPrefix + result);
|
|
3506
|
+
} else {
|
|
3507
|
+
resolve7({ logs: logBuffer.join("\n"), result });
|
|
3508
|
+
}
|
|
3509
|
+
} else {
|
|
3510
|
+
resolve7(result);
|
|
3511
|
+
}
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
const combined = (logBuffer.join("\n") + "\n" + stderr).trim();
|
|
3515
|
+
if (code !== 0) {
|
|
3516
|
+
reject(new Error(
|
|
3517
|
+
`Custom tool "${this.name}" exited with code ${code}. Output: ${combined || "(none)"}`
|
|
3518
|
+
));
|
|
3519
|
+
} else {
|
|
3520
|
+
resolve7(combined || `Tool "${this.name}" produced no output.`);
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
3523
|
+
child.on("error", (err) => {
|
|
3524
|
+
clearTimeout(timer);
|
|
3525
|
+
reject(new Error(
|
|
3526
|
+
`Failed to spawn custom tool "${this.name}": ${err.message}`
|
|
3527
|
+
));
|
|
3528
|
+
});
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
// --- internal line parser ---
|
|
3532
|
+
processLine(line, handlers) {
|
|
3533
|
+
const trimmed = line.trimEnd();
|
|
3534
|
+
if (!trimmed) return;
|
|
3535
|
+
if (trimmed.startsWith(TOOL_RESULT_PREFIX)) {
|
|
3536
|
+
const json = trimmed.slice(TOOL_RESULT_PREFIX.length).trim();
|
|
3537
|
+
try {
|
|
3538
|
+
handlers.onResult(JSON.parse(json));
|
|
3539
|
+
} catch {
|
|
3540
|
+
handlers.onResult(json);
|
|
3541
|
+
}
|
|
3542
|
+
} else if (trimmed.startsWith(TOOL_ERROR_PREFIX)) {
|
|
3543
|
+
handlers.onError(trimmed.slice(TOOL_ERROR_PREFIX.length).trim());
|
|
3544
|
+
} else if (trimmed.startsWith(TOOL_OUTPUT_PREFIX)) {
|
|
3545
|
+
handlers.onOutput(trimmed.slice(TOOL_OUTPUT_PREFIX.length).trim());
|
|
3546
|
+
} else {
|
|
3547
|
+
handlers.onOutput(trimmed);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3035
3550
|
};
|
|
3036
3551
|
|
|
3552
|
+
// src/tools/CustomToolScanner.ts
|
|
3553
|
+
var METADATA_REGEX = /export\s+const\s+TOOL_METADATA\s*=\s*(\{[\s\S]*?\});\s*$/m;
|
|
3554
|
+
function scanCustomTools(folderPath) {
|
|
3555
|
+
if (!fs7.existsSync(folderPath)) {
|
|
3556
|
+
throw new Error(
|
|
3557
|
+
`Custom tools folder not found: ${folderPath}. Create the directory or check your --custom-tools-folder path.`
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
const entries = fs7.readdirSync(folderPath, { withFileTypes: true });
|
|
3561
|
+
const discovered = [];
|
|
3562
|
+
for (const entry of entries) {
|
|
3563
|
+
if (entry.isDirectory()) continue;
|
|
3564
|
+
const ext = path7.extname(entry.name).toLowerCase();
|
|
3565
|
+
if (ext !== ".ts" && ext !== ".js") continue;
|
|
3566
|
+
const filePath = path7.resolve(folderPath, entry.name);
|
|
3567
|
+
const baseName = path7.basename(entry.name, ext);
|
|
3568
|
+
let metadata;
|
|
3569
|
+
try {
|
|
3570
|
+
metadata = extractMetadata(filePath);
|
|
3571
|
+
} catch (err) {
|
|
3572
|
+
throw new Error(
|
|
3573
|
+
`Failed to extract TOOL_METADATA from "${entry.name}": ${err.message}
|
|
3574
|
+
Ensure the file exports \`export const TOOL_METADATA = { name, description, inputs, outputType };\``
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
if (metadata.name !== baseName) {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
`Tool metadata name mismatch in "${entry.name}": file is "${baseName}" but TOOL_METADATA.name is "${metadata.name}". They must match (Convention over Configuration).`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
discovered.push({ filePath, metadata });
|
|
3583
|
+
}
|
|
3584
|
+
return discovered;
|
|
3585
|
+
}
|
|
3586
|
+
function extractMetadata(filePath) {
|
|
3587
|
+
const source = fs7.readFileSync(filePath, "utf8");
|
|
3588
|
+
const match = source.match(METADATA_REGEX);
|
|
3589
|
+
if (!match) {
|
|
3590
|
+
throw new Error(
|
|
3591
|
+
"No `export const TOOL_METADATA = { ... };` block found. Add the metadata export at the bottom of your tool file."
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
let parsed;
|
|
3595
|
+
try {
|
|
3596
|
+
parsed = new Function(`"use strict"; return (${match[1]});`)();
|
|
3597
|
+
} catch (err) {
|
|
3598
|
+
throw new Error(
|
|
3599
|
+
`Could not parse TOOL_METADATA object: ${err.message}. Ensure it is a valid JavaScript object literal.`
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
if (!parsed.name || typeof parsed.name !== "string") {
|
|
3603
|
+
throw new Error("TOOL_METADATA.name must be a non-empty string.");
|
|
3604
|
+
}
|
|
3605
|
+
if (!parsed.description || typeof parsed.description !== "string") {
|
|
3606
|
+
throw new Error("TOOL_METADATA.description must be a non-empty string.");
|
|
3607
|
+
}
|
|
3608
|
+
if (!parsed.inputs || typeof parsed.inputs !== "object") {
|
|
3609
|
+
throw new Error("TOOL_METADATA.inputs must be an object mapping parameter names to their schemas.");
|
|
3610
|
+
}
|
|
3611
|
+
if (!parsed.outputType || typeof parsed.outputType !== "string") {
|
|
3612
|
+
throw new Error("TOOL_METADATA.outputType must be a non-empty string.");
|
|
3613
|
+
}
|
|
3614
|
+
return parsed;
|
|
3615
|
+
}
|
|
3616
|
+
function loadCustomTools(folderPath) {
|
|
3617
|
+
const discovered = scanCustomTools(folderPath);
|
|
3618
|
+
const tools = /* @__PURE__ */ new Map();
|
|
3619
|
+
for (const { filePath, metadata } of discovered) {
|
|
3620
|
+
const config = {
|
|
3621
|
+
toolPath: filePath,
|
|
3622
|
+
name: metadata.name,
|
|
3623
|
+
description: metadata.description,
|
|
3624
|
+
inputs: metadata.inputs,
|
|
3625
|
+
outputType: metadata.outputType,
|
|
3626
|
+
timeout: metadata.timeout
|
|
3627
|
+
};
|
|
3628
|
+
tools.set(metadata.name, new ProxyTool(config));
|
|
3629
|
+
}
|
|
3630
|
+
return tools;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3037
3633
|
// src/cli.ts
|
|
3038
3634
|
import_dotenv.default.config();
|
|
3039
3635
|
async function main() {
|
|
@@ -3063,14 +3659,20 @@ function printUsage() {
|
|
|
3063
3659
|
console.log(" smol-js validate <workflow.yaml> Validate a workflow file");
|
|
3064
3660
|
console.log("");
|
|
3065
3661
|
console.log("Options:");
|
|
3066
|
-
console.log(" --task, -t <task>
|
|
3067
|
-
console.log(" --quiet, -q
|
|
3068
|
-
console.log(" --
|
|
3662
|
+
console.log(" --task, -t <task> Task description (prompted if not provided)");
|
|
3663
|
+
console.log(" --quiet, -q Reduce output verbosity");
|
|
3664
|
+
console.log(" --output-format <format> Output format: text (default) or json");
|
|
3665
|
+
console.log(" --verbose Include full step details in output");
|
|
3666
|
+
console.log(" --run-id <id> Unique run identifier (auto-generated if not provided)");
|
|
3667
|
+
console.log(" --cwd <dir> Working directory for agent file operations");
|
|
3668
|
+
console.log(" --custom-tools-folder <path> Path to folder containing standalone custom tool files");
|
|
3669
|
+
console.log(" --help, -h Show this help message");
|
|
3069
3670
|
console.log("");
|
|
3070
3671
|
console.log("Examples:");
|
|
3071
3672
|
console.log(' npx @samrahimi/smol-js workflow.yaml --task "Research AI safety"');
|
|
3072
3673
|
console.log(' smol-js research-agent.yaml -t "Write a summary of quantum computing"');
|
|
3073
3674
|
console.log(" smol-js validate my-workflow.yaml");
|
|
3675
|
+
console.log(' smol-js workflow.yaml --task "Task" --output-format json --run-id abc123');
|
|
3074
3676
|
}
|
|
3075
3677
|
async function runCommand(args) {
|
|
3076
3678
|
if (args.length === 0) {
|
|
@@ -3080,39 +3682,130 @@ async function runCommand(args) {
|
|
|
3080
3682
|
const filePath = args[0];
|
|
3081
3683
|
let task = "";
|
|
3082
3684
|
let quiet = false;
|
|
3685
|
+
let outputFormat = "text";
|
|
3686
|
+
let verbose = false;
|
|
3687
|
+
let runId = "";
|
|
3688
|
+
let cwd = "";
|
|
3689
|
+
let customToolsFolder = "";
|
|
3083
3690
|
for (let i = 1; i < args.length; i++) {
|
|
3084
3691
|
if (args[i] === "--task" || args[i] === "-t") {
|
|
3085
3692
|
task = args[i + 1] ?? "";
|
|
3086
3693
|
i++;
|
|
3087
3694
|
} else if (args[i] === "--quiet" || args[i] === "-q") {
|
|
3088
3695
|
quiet = true;
|
|
3696
|
+
} else if (args[i] === "--output-format") {
|
|
3697
|
+
const format = args[i + 1];
|
|
3698
|
+
if (format === "json" || format === "text") {
|
|
3699
|
+
outputFormat = format;
|
|
3700
|
+
} else {
|
|
3701
|
+
console.error(import_chalk3.default.red(`Invalid output format: ${format}. Use 'text' or 'json'.`));
|
|
3702
|
+
process.exit(1);
|
|
3703
|
+
}
|
|
3704
|
+
i++;
|
|
3705
|
+
} else if (args[i] === "--verbose") {
|
|
3706
|
+
verbose = true;
|
|
3707
|
+
} else if (args[i] === "--run-id") {
|
|
3708
|
+
runId = args[i + 1] ?? "";
|
|
3709
|
+
i++;
|
|
3710
|
+
} else if (args[i] === "--cwd") {
|
|
3711
|
+
cwd = args[i + 1] ?? "";
|
|
3712
|
+
i++;
|
|
3713
|
+
} else if (args[i] === "--custom-tools-folder") {
|
|
3714
|
+
customToolsFolder = args[i + 1] ?? "";
|
|
3715
|
+
i++;
|
|
3089
3716
|
}
|
|
3090
3717
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3718
|
+
if (outputFormat === "json" && !runId) {
|
|
3719
|
+
runId = `run-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
3720
|
+
}
|
|
3721
|
+
const resolvedPath = path8.isAbsolute(filePath) ? filePath : path8.resolve(process.cwd(), filePath);
|
|
3722
|
+
if (!fs8.existsSync(resolvedPath)) {
|
|
3723
|
+
if (outputFormat === "json") {
|
|
3724
|
+
console.log(JSON.stringify({
|
|
3725
|
+
runId: runId || "unknown",
|
|
3726
|
+
timestamp: Date.now(),
|
|
3727
|
+
type: "error",
|
|
3728
|
+
data: { message: `File not found: ${resolvedPath}` }
|
|
3729
|
+
}));
|
|
3730
|
+
} else {
|
|
3731
|
+
console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
|
|
3732
|
+
}
|
|
3094
3733
|
process.exit(1);
|
|
3095
3734
|
}
|
|
3096
3735
|
if (!task) {
|
|
3736
|
+
if (outputFormat === "json") {
|
|
3737
|
+
console.log(JSON.stringify({
|
|
3738
|
+
runId: runId || "unknown",
|
|
3739
|
+
timestamp: Date.now(),
|
|
3740
|
+
type: "error",
|
|
3741
|
+
data: { message: "Task is required in JSON output mode. Use --task flag." }
|
|
3742
|
+
}));
|
|
3743
|
+
process.exit(1);
|
|
3744
|
+
}
|
|
3097
3745
|
task = await promptUser("Enter your task: ");
|
|
3098
3746
|
if (!task.trim()) {
|
|
3099
3747
|
console.error(import_chalk3.default.red("Error: task cannot be empty"));
|
|
3100
3748
|
process.exit(1);
|
|
3101
3749
|
}
|
|
3102
3750
|
}
|
|
3103
|
-
|
|
3751
|
+
if (cwd) {
|
|
3752
|
+
const resolvedCwd = path8.isAbsolute(cwd) ? cwd : path8.resolve(process.cwd(), cwd);
|
|
3753
|
+
if (!fs8.existsSync(resolvedCwd)) {
|
|
3754
|
+
fs8.mkdirSync(resolvedCwd, { recursive: true });
|
|
3755
|
+
}
|
|
3756
|
+
process.chdir(resolvedCwd);
|
|
3757
|
+
}
|
|
3758
|
+
const orchestrator = new Orchestrator({
|
|
3759
|
+
verbose: outputFormat === "json" ? verbose : !quiet,
|
|
3760
|
+
outputFormat,
|
|
3761
|
+
runId: runId || void 0,
|
|
3762
|
+
cwd: cwd || void 0
|
|
3763
|
+
});
|
|
3764
|
+
if (customToolsFolder) {
|
|
3765
|
+
const resolvedToolsFolder = path8.isAbsolute(customToolsFolder) ? customToolsFolder : path8.resolve(process.cwd(), customToolsFolder);
|
|
3766
|
+
if (outputFormat !== "json") {
|
|
3767
|
+
console.log(import_chalk3.default.gray(`
|
|
3768
|
+
Scanning custom tools in: ${resolvedToolsFolder}
|
|
3769
|
+
`));
|
|
3770
|
+
}
|
|
3771
|
+
const customTools = loadCustomTools(resolvedToolsFolder);
|
|
3772
|
+
const loader = orchestrator.getLoader();
|
|
3773
|
+
for (const [toolName, proxyTool] of customTools) {
|
|
3774
|
+
loader.registerToolInstance(toolName, proxyTool);
|
|
3775
|
+
if (outputFormat !== "json") {
|
|
3776
|
+
console.log(import_chalk3.default.green(` + Registered custom tool: ${toolName}`));
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3104
3780
|
try {
|
|
3105
|
-
|
|
3781
|
+
if (outputFormat !== "json") {
|
|
3782
|
+
console.log(import_chalk3.default.gray(`
|
|
3106
3783
|
Loading workflow from: ${resolvedPath}
|
|
3107
3784
|
`));
|
|
3785
|
+
}
|
|
3108
3786
|
const workflow = orchestrator.loadWorkflow(resolvedPath);
|
|
3109
|
-
await orchestrator.runWorkflow(workflow, task);
|
|
3787
|
+
await orchestrator.runWorkflow(workflow, task, resolvedPath);
|
|
3110
3788
|
process.exit(0);
|
|
3111
3789
|
} catch (error) {
|
|
3112
|
-
|
|
3790
|
+
if (outputFormat === "json") {
|
|
3791
|
+
const jsonHandler = orchestrator.getJSONOutputHandler();
|
|
3792
|
+
if (jsonHandler) {
|
|
3793
|
+
jsonHandler.emitError(error.message, error.stack);
|
|
3794
|
+
jsonHandler.emitRunEnd(false, null, 0, 0);
|
|
3795
|
+
} else {
|
|
3796
|
+
console.log(JSON.stringify({
|
|
3797
|
+
runId: runId || "unknown",
|
|
3798
|
+
timestamp: Date.now(),
|
|
3799
|
+
type: "error",
|
|
3800
|
+
data: { message: error.message, stack: error.stack }
|
|
3801
|
+
}));
|
|
3802
|
+
}
|
|
3803
|
+
} else {
|
|
3804
|
+
console.error(import_chalk3.default.red(`
|
|
3113
3805
|
Error: ${error.message}`));
|
|
3114
|
-
|
|
3115
|
-
|
|
3806
|
+
if (process.env.DEBUG) {
|
|
3807
|
+
console.error(error.stack);
|
|
3808
|
+
}
|
|
3116
3809
|
}
|
|
3117
3810
|
process.exit(1);
|
|
3118
3811
|
}
|
|
@@ -3123,8 +3816,8 @@ async function validateCommand(args) {
|
|
|
3123
3816
|
process.exit(1);
|
|
3124
3817
|
}
|
|
3125
3818
|
const filePath = args[0];
|
|
3126
|
-
const resolvedPath =
|
|
3127
|
-
if (!
|
|
3819
|
+
const resolvedPath = path8.isAbsolute(filePath) ? filePath : path8.resolve(process.cwd(), filePath);
|
|
3820
|
+
if (!fs8.existsSync(resolvedPath)) {
|
|
3128
3821
|
console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
|
|
3129
3822
|
process.exit(1);
|
|
3130
3823
|
}
|
|
@@ -3147,10 +3840,10 @@ function promptUser(question) {
|
|
|
3147
3840
|
input: process.stdin,
|
|
3148
3841
|
output: process.stdout
|
|
3149
3842
|
});
|
|
3150
|
-
return new Promise((
|
|
3843
|
+
return new Promise((resolve7) => {
|
|
3151
3844
|
rl.question(import_chalk3.default.cyan(question), (answer) => {
|
|
3152
3845
|
rl.close();
|
|
3153
|
-
|
|
3846
|
+
resolve7(answer);
|
|
3154
3847
|
});
|
|
3155
3848
|
});
|
|
3156
3849
|
}
|