@samrahimi/smol-js 0.6.4 → 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 -134
- 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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
"use strict";
|
|
4
3
|
var __create = Object.create;
|
|
5
4
|
var __defProp = Object.defineProperty;
|
|
@@ -25,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
24
|
));
|
|
26
25
|
|
|
27
26
|
// src/cli.ts
|
|
28
|
-
var
|
|
29
|
-
var
|
|
27
|
+
var fs8 = __toESM(require("fs"));
|
|
28
|
+
var path8 = __toESM(require("path"));
|
|
30
29
|
var readline = __toESM(require("readline"));
|
|
31
30
|
var import_chalk3 = __toESM(require("chalk"));
|
|
32
31
|
var import_dotenv = __toESM(require("dotenv"));
|
|
@@ -742,9 +741,21 @@ Total time: ${(duration / 1e3).toFixed(2)}s`);
|
|
|
742
741
|
getName() {
|
|
743
742
|
return this.config.name;
|
|
744
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
|
+
}
|
|
745
756
|
/** Sleep for a specified duration */
|
|
746
757
|
sleep(ms) {
|
|
747
|
-
return new Promise((
|
|
758
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
748
759
|
}
|
|
749
760
|
};
|
|
750
761
|
|
|
@@ -1847,6 +1858,10 @@ Please try a different approach.`
|
|
|
1847
1858
|
memoryStep.tokenUsage = response.tokenUsage;
|
|
1848
1859
|
if (response.content && response.content.trim()) {
|
|
1849
1860
|
this.logger.reasoning(response.content.trim());
|
|
1861
|
+
this.emitEvent("agent_thinking", {
|
|
1862
|
+
step: this.currentStep,
|
|
1863
|
+
content: response.content.trim()
|
|
1864
|
+
});
|
|
1850
1865
|
}
|
|
1851
1866
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1852
1867
|
this.logger.warn("No tool calls in response. Prompting model to use tools.");
|
|
@@ -1879,42 +1894,84 @@ Please try a different approach.`
|
|
|
1879
1894
|
async processToolCalls(toolCalls) {
|
|
1880
1895
|
const results = [];
|
|
1881
1896
|
const executeTool = async (tc) => {
|
|
1897
|
+
const startTime = Date.now();
|
|
1882
1898
|
const toolName = tc.function.name;
|
|
1883
1899
|
const tool = this.tools.get(toolName);
|
|
1884
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
|
+
});
|
|
1885
1910
|
return {
|
|
1886
1911
|
toolCallId: tc.id,
|
|
1887
1912
|
toolName,
|
|
1888
1913
|
result: null,
|
|
1889
|
-
error
|
|
1914
|
+
error
|
|
1890
1915
|
};
|
|
1891
1916
|
}
|
|
1892
1917
|
let args;
|
|
1893
1918
|
try {
|
|
1894
1919
|
args = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
|
|
1895
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
|
+
});
|
|
1896
1930
|
return {
|
|
1897
1931
|
toolCallId: tc.id,
|
|
1898
1932
|
toolName,
|
|
1899
1933
|
result: null,
|
|
1900
|
-
error
|
|
1934
|
+
error
|
|
1901
1935
|
};
|
|
1902
1936
|
}
|
|
1903
1937
|
this.logger.info(` Calling tool: ${toolName}(${JSON.stringify(args).slice(0, 100)}...)`);
|
|
1904
|
-
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
|
+
});
|
|
1905
1944
|
try {
|
|
1906
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
|
+
});
|
|
1907
1954
|
return {
|
|
1908
1955
|
toolCallId: tc.id,
|
|
1909
1956
|
toolName,
|
|
1910
1957
|
result
|
|
1911
1958
|
};
|
|
1912
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
|
+
});
|
|
1913
1970
|
return {
|
|
1914
1971
|
toolCallId: tc.id,
|
|
1915
1972
|
toolName,
|
|
1916
1973
|
result: null,
|
|
1917
|
-
error:
|
|
1974
|
+
error: errorMsg
|
|
1918
1975
|
};
|
|
1919
1976
|
}
|
|
1920
1977
|
};
|
|
@@ -2037,7 +2094,7 @@ async function ${this.name}(task: string): Promise<string> { ... }
|
|
|
2037
2094
|
};
|
|
2038
2095
|
|
|
2039
2096
|
// src/models/OpenAIModel.ts
|
|
2040
|
-
var import_openai = __toESM(require("openai"));
|
|
2097
|
+
var import_openai = __toESM(require("openai/index.mjs"));
|
|
2041
2098
|
|
|
2042
2099
|
// src/models/Model.ts
|
|
2043
2100
|
var Model = class {
|
|
@@ -2580,40 +2637,35 @@ var ExaGetContentsTool = class extends Tool {
|
|
|
2580
2637
|
// src/tools/ExaResearchTool.ts
|
|
2581
2638
|
var ExaResearchTool = class extends Tool {
|
|
2582
2639
|
name = "exa_research";
|
|
2583
|
-
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.";
|
|
2584
2641
|
inputs = {
|
|
2585
|
-
|
|
2642
|
+
instructions: {
|
|
2586
2643
|
type: "string",
|
|
2587
|
-
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.",
|
|
2588
2645
|
required: true
|
|
2589
2646
|
},
|
|
2590
|
-
|
|
2591
|
-
type: "number",
|
|
2592
|
-
description: "Number of primary sources to retrieve (default: 5, max: 10)",
|
|
2593
|
-
required: false,
|
|
2594
|
-
default: 5
|
|
2595
|
-
},
|
|
2596
|
-
category: {
|
|
2647
|
+
model: {
|
|
2597
2648
|
type: "string",
|
|
2598
|
-
description: '
|
|
2599
|
-
required: false
|
|
2600
|
-
},
|
|
2601
|
-
includeDomains: {
|
|
2602
|
-
type: "array",
|
|
2603
|
-
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)',
|
|
2604
2650
|
required: false
|
|
2605
2651
|
},
|
|
2606
|
-
|
|
2607
|
-
type: "
|
|
2608
|
-
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.",
|
|
2609
2655
|
required: false
|
|
2610
2656
|
}
|
|
2611
2657
|
};
|
|
2612
2658
|
outputType = "string";
|
|
2613
2659
|
apiKey;
|
|
2660
|
+
defaultModel;
|
|
2661
|
+
pollInterval;
|
|
2662
|
+
maxPollTime;
|
|
2614
2663
|
constructor(config) {
|
|
2615
2664
|
super();
|
|
2616
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;
|
|
2617
2669
|
}
|
|
2618
2670
|
async setup() {
|
|
2619
2671
|
if (!this.apiKey) {
|
|
@@ -2622,91 +2674,87 @@ var ExaResearchTool = class extends Tool {
|
|
|
2622
2674
|
this.isSetup = true;
|
|
2623
2675
|
}
|
|
2624
2676
|
async execute(args) {
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
|
2632
2686
|
};
|
|
2633
|
-
if (
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
const
|
|
2687
|
+
if (outputSchema) {
|
|
2688
|
+
createBody.outputSchema = outputSchema;
|
|
2689
|
+
}
|
|
2690
|
+
const createResponse = await fetch("https://api.exa.ai/research/v1", {
|
|
2637
2691
|
method: "POST",
|
|
2638
2692
|
headers: {
|
|
2639
2693
|
"x-api-key": this.apiKey,
|
|
2640
2694
|
"Content-Type": "application/json"
|
|
2641
2695
|
},
|
|
2642
|
-
body: JSON.stringify(
|
|
2696
|
+
body: JSON.stringify(createBody)
|
|
2643
2697
|
});
|
|
2644
|
-
if (!
|
|
2645
|
-
const errorText = await
|
|
2646
|
-
throw new Error(`
|
|
2698
|
+
if (!createResponse.ok) {
|
|
2699
|
+
const errorText = await createResponse.text();
|
|
2700
|
+
throw new Error(`Failed to create research task (${createResponse.status}): ${errorText}`);
|
|
2647
2701
|
}
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
let
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
headers: {
|
|
2663
|
-
"x-api-key": this.apiKey,
|
|
2664
|
-
"Content-Type": "application/json"
|
|
2665
|
-
},
|
|
2666
|
-
body: JSON.stringify(similarBody)
|
|
2667
|
-
});
|
|
2668
|
-
if (similarResponse.ok) {
|
|
2669
|
-
const similarData = await similarResponse.json();
|
|
2670
|
-
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
|
|
2671
2716
|
}
|
|
2672
|
-
}
|
|
2717
|
+
});
|
|
2718
|
+
if (!statusResponse.ok) {
|
|
2719
|
+
const errorText = await statusResponse.text();
|
|
2720
|
+
throw new Error(`Failed to check research status (${statusResponse.status}): ${errorText}`);
|
|
2673
2721
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
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");
|
|
2694
2746
|
}
|
|
2695
|
-
if ("
|
|
2696
|
-
|
|
2747
|
+
if (statusData.status === "failed") {
|
|
2748
|
+
throw new Error(`Research failed: ${statusData.error ?? "Unknown error"}`);
|
|
2697
2749
|
}
|
|
2698
|
-
if (
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2750
|
+
if (statusData.status === "canceled") {
|
|
2751
|
+
throw new Error("Research was canceled");
|
|
2752
|
+
}
|
|
2753
|
+
if (attempts % 10 === 0) {
|
|
2754
|
+
console.log(`Still researching... (${attempts} polls, ${((Date.now() - startTime) / 1e3).toFixed(0)}s elapsed)`);
|
|
2702
2755
|
}
|
|
2703
|
-
sections.push("");
|
|
2704
2756
|
}
|
|
2705
|
-
|
|
2706
|
-
uniqueSources.forEach((s, i) => {
|
|
2707
|
-
sections.push(`${i + 1}. ${s.url}`);
|
|
2708
|
-
});
|
|
2709
|
-
return sections.join("\n");
|
|
2757
|
+
throw new Error(`Research timed out after ${this.maxPollTime / 1e3}s. Task ID: ${researchId}`);
|
|
2710
2758
|
}
|
|
2711
2759
|
};
|
|
2712
2760
|
|
|
@@ -2722,12 +2770,21 @@ var TOOL_REGISTRY = {
|
|
|
2722
2770
|
};
|
|
2723
2771
|
var YAMLLoader = class {
|
|
2724
2772
|
customTools = /* @__PURE__ */ new Map();
|
|
2773
|
+
toolInstances = /* @__PURE__ */ new Map();
|
|
2725
2774
|
/**
|
|
2726
|
-
* Register a custom tool type for use in YAML definitions.
|
|
2775
|
+
* Register a custom tool type (class) for use in YAML definitions.
|
|
2727
2776
|
*/
|
|
2728
2777
|
registerToolType(typeName, toolClass) {
|
|
2729
2778
|
this.customTools.set(typeName, toolClass);
|
|
2730
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
|
+
}
|
|
2731
2788
|
/**
|
|
2732
2789
|
* Load a workflow from a YAML file path.
|
|
2733
2790
|
*/
|
|
@@ -2811,9 +2868,16 @@ var YAMLLoader = class {
|
|
|
2811
2868
|
* Build a tool instance from a type name and config.
|
|
2812
2869
|
*/
|
|
2813
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
|
+
}
|
|
2814
2878
|
const ToolClass = TOOL_REGISTRY[type] ?? this.customTools.get(type);
|
|
2815
2879
|
if (!ToolClass) {
|
|
2816
|
-
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(", ")}`);
|
|
2817
2881
|
}
|
|
2818
2882
|
const tool = new ToolClass(config);
|
|
2819
2883
|
if (name !== type && name !== tool.name) {
|
|
@@ -2841,11 +2905,16 @@ var YAMLLoader = class {
|
|
|
2841
2905
|
if (tool) {
|
|
2842
2906
|
agentTools.push(tool);
|
|
2843
2907
|
} else {
|
|
2844
|
-
const
|
|
2845
|
-
if (
|
|
2846
|
-
agentTools.push(
|
|
2908
|
+
const instance = this.toolInstances.get(toolName);
|
|
2909
|
+
if (instance) {
|
|
2910
|
+
agentTools.push(instance);
|
|
2847
2911
|
} else {
|
|
2848
|
-
|
|
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
|
+
}
|
|
2849
2918
|
}
|
|
2850
2919
|
}
|
|
2851
2920
|
}
|
|
@@ -2894,40 +2963,116 @@ var YAMLLoader = class {
|
|
|
2894
2963
|
}
|
|
2895
2964
|
};
|
|
2896
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
|
+
|
|
2897
3027
|
// src/orchestrator/Orchestrator.ts
|
|
2898
3028
|
var Orchestrator = class {
|
|
2899
3029
|
loader;
|
|
2900
3030
|
config;
|
|
2901
3031
|
activeAgents = /* @__PURE__ */ new Map();
|
|
2902
3032
|
eventLog = [];
|
|
3033
|
+
jsonOutput = null;
|
|
3034
|
+
isJsonMode = false;
|
|
2903
3035
|
constructor(config = {}) {
|
|
2904
3036
|
this.loader = new YAMLLoader();
|
|
3037
|
+
this.isJsonMode = config.outputFormat === "json";
|
|
2905
3038
|
this.config = {
|
|
2906
3039
|
verbose: config.verbose ?? true,
|
|
2907
|
-
onEvent: config.onEvent
|
|
3040
|
+
onEvent: config.onEvent,
|
|
3041
|
+
outputFormat: config.outputFormat ?? "text",
|
|
3042
|
+
runId: config.runId,
|
|
3043
|
+
cwd: config.cwd
|
|
2908
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
|
+
}
|
|
2909
3054
|
}
|
|
2910
3055
|
/**
|
|
2911
3056
|
* Load a workflow from a YAML file.
|
|
2912
3057
|
*/
|
|
2913
3058
|
loadWorkflow(filePath) {
|
|
2914
3059
|
const workflow = this.loader.loadFromFile(filePath);
|
|
2915
|
-
this.displayWorkflowInfo(workflow);
|
|
3060
|
+
this.displayWorkflowInfo(workflow, filePath);
|
|
2916
3061
|
return workflow;
|
|
2917
3062
|
}
|
|
2918
3063
|
/**
|
|
2919
3064
|
* Load a workflow from YAML string.
|
|
2920
3065
|
*/
|
|
2921
|
-
loadWorkflowFromString(yamlContent) {
|
|
3066
|
+
loadWorkflowFromString(yamlContent, sourcePath) {
|
|
2922
3067
|
const workflow = this.loader.loadFromString(yamlContent);
|
|
2923
|
-
this.displayWorkflowInfo(workflow);
|
|
3068
|
+
this.displayWorkflowInfo(workflow, sourcePath);
|
|
2924
3069
|
return workflow;
|
|
2925
3070
|
}
|
|
2926
3071
|
/**
|
|
2927
3072
|
* Run a loaded workflow with a task.
|
|
2928
3073
|
*/
|
|
2929
|
-
async runWorkflow(workflow, task) {
|
|
2930
|
-
this.displayRunStart(workflow.name, task);
|
|
3074
|
+
async runWorkflow(workflow, task, workflowPath) {
|
|
3075
|
+
this.displayRunStart(workflow.name, task, workflowPath);
|
|
2931
3076
|
this.instrumentAgent(workflow.entrypointAgent, workflow.entrypointAgent.getName(), 0);
|
|
2932
3077
|
for (const [name, agent] of workflow.agents) {
|
|
2933
3078
|
if (agent !== workflow.entrypointAgent) {
|
|
@@ -2936,10 +3081,13 @@ var Orchestrator = class {
|
|
|
2936
3081
|
}
|
|
2937
3082
|
try {
|
|
2938
3083
|
const result = await workflow.entrypointAgent.run(task);
|
|
2939
|
-
this.displayRunEnd(result);
|
|
3084
|
+
this.displayRunEnd(result, true);
|
|
2940
3085
|
return result;
|
|
2941
3086
|
} catch (error) {
|
|
2942
3087
|
this.displayError(error);
|
|
3088
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3089
|
+
this.jsonOutput.emitRunEnd(false, null, 0, 0);
|
|
3090
|
+
}
|
|
2943
3091
|
throw error;
|
|
2944
3092
|
}
|
|
2945
3093
|
}
|
|
@@ -2956,11 +3104,128 @@ var Orchestrator = class {
|
|
|
2956
3104
|
*/
|
|
2957
3105
|
instrumentAgent(agent, name, depth) {
|
|
2958
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
|
+
}
|
|
2959
3211
|
}
|
|
2960
3212
|
/**
|
|
2961
3213
|
* Display workflow info at startup.
|
|
2962
3214
|
*/
|
|
2963
|
-
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
|
+
}
|
|
2964
3229
|
if (!this.config.verbose) return;
|
|
2965
3230
|
const line = "\u2550".repeat(70);
|
|
2966
3231
|
console.log(import_chalk2.default.cyan(line));
|
|
@@ -2968,16 +3233,20 @@ var Orchestrator = class {
|
|
|
2968
3233
|
if (workflow.description) {
|
|
2969
3234
|
console.log(import_chalk2.default.cyan(` ${workflow.description}`));
|
|
2970
3235
|
}
|
|
2971
|
-
console.log(import_chalk2.default.cyan(` Agents: ${
|
|
2972
|
-
console.log(import_chalk2.default.cyan(` Tools: ${
|
|
2973
|
-
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}`));
|
|
2974
3239
|
console.log(import_chalk2.default.cyan(line));
|
|
2975
3240
|
console.log();
|
|
2976
3241
|
}
|
|
2977
3242
|
/**
|
|
2978
3243
|
* Display run start info.
|
|
2979
3244
|
*/
|
|
2980
|
-
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
|
+
}
|
|
2981
3250
|
if (!this.config.verbose) return;
|
|
2982
3251
|
console.log(import_chalk2.default.green.bold(`
|
|
2983
3252
|
\u25B6 Running workflow "${workflowName}"`));
|
|
@@ -2987,7 +3256,16 @@ var Orchestrator = class {
|
|
|
2987
3256
|
/**
|
|
2988
3257
|
* Display run completion info.
|
|
2989
3258
|
*/
|
|
2990
|
-
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
|
+
}
|
|
2991
3269
|
if (!this.config.verbose) return;
|
|
2992
3270
|
console.log(import_chalk2.default.gray("\n" + "\u2500".repeat(70)));
|
|
2993
3271
|
console.log(import_chalk2.default.green.bold(`
|
|
@@ -3005,6 +3283,10 @@ var Orchestrator = class {
|
|
|
3005
3283
|
* Display an error.
|
|
3006
3284
|
*/
|
|
3007
3285
|
displayError(error) {
|
|
3286
|
+
if (this.isJsonMode && this.jsonOutput) {
|
|
3287
|
+
this.jsonOutput.emitError(error.message, error.stack);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3008
3290
|
if (!this.config.verbose) return;
|
|
3009
3291
|
console.error(import_chalk2.default.red.bold(`
|
|
3010
3292
|
\u274C Workflow failed: ${error.message}`));
|
|
@@ -3033,8 +3315,321 @@ var Orchestrator = class {
|
|
|
3033
3315
|
getLoader() {
|
|
3034
3316
|
return this.loader;
|
|
3035
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
|
+
}
|
|
3036
3336
|
};
|
|
3037
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
|
+
}
|
|
3550
|
+
};
|
|
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
|
+
|
|
3038
3633
|
// src/cli.ts
|
|
3039
3634
|
import_dotenv.default.config();
|
|
3040
3635
|
async function main() {
|
|
@@ -3064,14 +3659,20 @@ function printUsage() {
|
|
|
3064
3659
|
console.log(" smol-js validate <workflow.yaml> Validate a workflow file");
|
|
3065
3660
|
console.log("");
|
|
3066
3661
|
console.log("Options:");
|
|
3067
|
-
console.log(" --task, -t <task>
|
|
3068
|
-
console.log(" --quiet, -q
|
|
3069
|
-
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");
|
|
3070
3670
|
console.log("");
|
|
3071
3671
|
console.log("Examples:");
|
|
3072
3672
|
console.log(' npx @samrahimi/smol-js workflow.yaml --task "Research AI safety"');
|
|
3073
3673
|
console.log(' smol-js research-agent.yaml -t "Write a summary of quantum computing"');
|
|
3074
3674
|
console.log(" smol-js validate my-workflow.yaml");
|
|
3675
|
+
console.log(' smol-js workflow.yaml --task "Task" --output-format json --run-id abc123');
|
|
3075
3676
|
}
|
|
3076
3677
|
async function runCommand(args) {
|
|
3077
3678
|
if (args.length === 0) {
|
|
@@ -3081,39 +3682,130 @@ async function runCommand(args) {
|
|
|
3081
3682
|
const filePath = args[0];
|
|
3082
3683
|
let task = "";
|
|
3083
3684
|
let quiet = false;
|
|
3685
|
+
let outputFormat = "text";
|
|
3686
|
+
let verbose = false;
|
|
3687
|
+
let runId = "";
|
|
3688
|
+
let cwd = "";
|
|
3689
|
+
let customToolsFolder = "";
|
|
3084
3690
|
for (let i = 1; i < args.length; i++) {
|
|
3085
3691
|
if (args[i] === "--task" || args[i] === "-t") {
|
|
3086
3692
|
task = args[i + 1] ?? "";
|
|
3087
3693
|
i++;
|
|
3088
3694
|
} else if (args[i] === "--quiet" || args[i] === "-q") {
|
|
3089
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++;
|
|
3090
3716
|
}
|
|
3091
3717
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
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
|
+
}
|
|
3095
3733
|
process.exit(1);
|
|
3096
3734
|
}
|
|
3097
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
|
+
}
|
|
3098
3745
|
task = await promptUser("Enter your task: ");
|
|
3099
3746
|
if (!task.trim()) {
|
|
3100
3747
|
console.error(import_chalk3.default.red("Error: task cannot be empty"));
|
|
3101
3748
|
process.exit(1);
|
|
3102
3749
|
}
|
|
3103
3750
|
}
|
|
3104
|
-
|
|
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
|
+
}
|
|
3105
3780
|
try {
|
|
3106
|
-
|
|
3781
|
+
if (outputFormat !== "json") {
|
|
3782
|
+
console.log(import_chalk3.default.gray(`
|
|
3107
3783
|
Loading workflow from: ${resolvedPath}
|
|
3108
3784
|
`));
|
|
3785
|
+
}
|
|
3109
3786
|
const workflow = orchestrator.loadWorkflow(resolvedPath);
|
|
3110
|
-
await orchestrator.runWorkflow(workflow, task);
|
|
3787
|
+
await orchestrator.runWorkflow(workflow, task, resolvedPath);
|
|
3111
3788
|
process.exit(0);
|
|
3112
3789
|
} catch (error) {
|
|
3113
|
-
|
|
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(`
|
|
3114
3805
|
Error: ${error.message}`));
|
|
3115
|
-
|
|
3116
|
-
|
|
3806
|
+
if (process.env.DEBUG) {
|
|
3807
|
+
console.error(error.stack);
|
|
3808
|
+
}
|
|
3117
3809
|
}
|
|
3118
3810
|
process.exit(1);
|
|
3119
3811
|
}
|
|
@@ -3124,8 +3816,8 @@ async function validateCommand(args) {
|
|
|
3124
3816
|
process.exit(1);
|
|
3125
3817
|
}
|
|
3126
3818
|
const filePath = args[0];
|
|
3127
|
-
const resolvedPath =
|
|
3128
|
-
if (!
|
|
3819
|
+
const resolvedPath = path8.isAbsolute(filePath) ? filePath : path8.resolve(process.cwd(), filePath);
|
|
3820
|
+
if (!fs8.existsSync(resolvedPath)) {
|
|
3129
3821
|
console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
|
|
3130
3822
|
process.exit(1);
|
|
3131
3823
|
}
|
|
@@ -3148,10 +3840,10 @@ function promptUser(question) {
|
|
|
3148
3840
|
input: process.stdin,
|
|
3149
3841
|
output: process.stdout
|
|
3150
3842
|
});
|
|
3151
|
-
return new Promise((
|
|
3843
|
+
return new Promise((resolve7) => {
|
|
3152
3844
|
rl.question(import_chalk3.default.cyan(question), (answer) => {
|
|
3153
3845
|
rl.close();
|
|
3154
|
-
|
|
3846
|
+
resolve7(answer);
|
|
3155
3847
|
});
|
|
3156
3848
|
});
|
|
3157
3849
|
}
|