mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-27 20:29:44 +02:00
everything is an agent
This commit is contained in:
parent
2d6a647c70
commit
80dae17fd1
24 changed files with 1261 additions and 1573 deletions
|
|
@ -1,4 +1,32 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import yargs from 'yargs';
|
||||||
|
import { hideBin } from 'yargs/helpers';
|
||||||
|
import { app } from '../dist/app.js';
|
||||||
|
|
||||||
import { start } from '../dist/x.js';
|
yargs(hideBin(process.argv))
|
||||||
start();
|
.command(
|
||||||
|
"$0",
|
||||||
|
"Run rowboatx",
|
||||||
|
(y) => y
|
||||||
|
.option("agent", {
|
||||||
|
type: "string",
|
||||||
|
description: "The agent to run",
|
||||||
|
default: "copilot",
|
||||||
|
})
|
||||||
|
.option("run_id", {
|
||||||
|
type: "string",
|
||||||
|
description: "Continue an existing run",
|
||||||
|
})
|
||||||
|
.option("input", {
|
||||||
|
type: "string",
|
||||||
|
description: "The input to the agent",
|
||||||
|
}),
|
||||||
|
(argv) => {
|
||||||
|
app({
|
||||||
|
agent: argv.agent,
|
||||||
|
runId: argv.run_id,
|
||||||
|
input: argv.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.parse();
|
||||||
159
apps/cli/package-lock.json
generated
159
apps/cli/package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
||||||
"ai": "^5.0.78",
|
"ai": "^5.0.78",
|
||||||
"json-schema-to-zod": "^2.6.1",
|
"json-schema-to-zod": "^2.6.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"yargs": "^18.0.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -372,6 +373,30 @@
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
|
@ -437,6 +462,20 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^7.2.0",
|
||||||
|
"strip-ansi": "^7.1.0",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
|
|
@ -566,6 +605,12 @@
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
|
@ -605,6 +650,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-html": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
|
@ -754,6 +808,27 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-east-asian-width": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
|
@ -1346,6 +1421,38 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -1483,12 +1590,64 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"string-width": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "18.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||||
|
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^9.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"string-width": "^7.2.0",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^22.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "22.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||||
|
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"ai": "^5.0.78",
|
"ai": "^5.0.78",
|
||||||
"json-schema-to-zod": "^2.6.1",
|
"json-schema-to-zod": "^2.6.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"yargs": "^18.0.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,19 @@
|
||||||
import { executeWorkflow, resumeWorkflow } from "./application/lib/exec-workflow.js";
|
import { streamAgent } from "./application/lib/agent.js";
|
||||||
import { StreamRenderer } from "./application/lib/stream-renderer.js";
|
import { StreamRenderer } from "./application/lib/stream-renderer.js";
|
||||||
import { createInterface } from "node:readline/promises";
|
|
||||||
import { stdin as input, stdout as output } from "node:process";
|
|
||||||
|
|
||||||
type ParsedArgs = {
|
export async function app(opts: {
|
||||||
command: "run" | "resume" | "help" | null;
|
agent: string;
|
||||||
id: string | null;
|
runId?: string;
|
||||||
interactive: boolean;
|
input?: string;
|
||||||
message: string;
|
}) {
|
||||||
};
|
|
||||||
|
|
||||||
function parseArgs(argv: string[]): ParsedArgs {
|
|
||||||
const args = argv.slice(2);
|
|
||||||
if (args.length === 0) {
|
|
||||||
return { command: "help", id: null, interactive: true, message: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
let command: ParsedArgs["command"] = null;
|
|
||||||
let id: string | null = null;
|
|
||||||
let interactive = true;
|
|
||||||
const messageParts: string[] = [];
|
|
||||||
|
|
||||||
if (args[0] !== "run" && args[0] !== "resume") {
|
|
||||||
command = "help";
|
|
||||||
return { command, id: null, interactive, message: "" };
|
|
||||||
}
|
|
||||||
command = args[0];
|
|
||||||
|
|
||||||
for (let i = 1; i < args.length; i++) {
|
|
||||||
const a = args[i];
|
|
||||||
if (a.startsWith("--")) {
|
|
||||||
if (a === "--no-interactive") {
|
|
||||||
interactive = false;
|
|
||||||
} else if (a.startsWith("--interactive")) {
|
|
||||||
const [, value] = a.split("=");
|
|
||||||
if (value === undefined) {
|
|
||||||
interactive = true;
|
|
||||||
} else {
|
|
||||||
interactive = value !== "false";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!id) {
|
|
||||||
id = a;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
messageParts.push(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { command, id, interactive, message: messageParts.join(" ") };
|
|
||||||
}
|
|
||||||
|
|
||||||
function printUsage(): void {
|
|
||||||
console.log([
|
|
||||||
"Usage:",
|
|
||||||
" rowboatx run <workflow_id> [message...] [--interactive | --no-interactive]",
|
|
||||||
" rowboatx resume <run_id> [message...] [--interactive | --no-interactive]",
|
|
||||||
"",
|
|
||||||
"Flags:",
|
|
||||||
" --interactive Run interactively (default: true)",
|
|
||||||
" --no-interactive Disable interactive prompts",
|
|
||||||
].join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promptForResumeInput(): Promise<string> {
|
|
||||||
const rl = createInterface({ input, output });
|
|
||||||
try {
|
|
||||||
const answer = await rl.question("Enter input to resume the run: ");
|
|
||||||
return answer;
|
|
||||||
} finally {
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function render(generator: AsyncGenerator<any, void, unknown>): Promise<void> {
|
|
||||||
const renderer = new StreamRenderer();
|
const renderer = new StreamRenderer();
|
||||||
for await (const event of generator) {
|
for await (const event of streamAgent({
|
||||||
|
...opts,
|
||||||
|
interactive: true,
|
||||||
|
})) {
|
||||||
renderer.render(event);
|
renderer.render(event);
|
||||||
if (event?.type === "error") {
|
if (event?.type === "error") {
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const { command, id, interactive, message } = parseArgs(process.argv);
|
|
||||||
|
|
||||||
if (command === "help" || !command) {
|
|
||||||
printUsage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!id) {
|
|
||||||
printUsage();
|
|
||||||
process.exitCode = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case "run": {
|
|
||||||
const initialInput = message ?? "";
|
|
||||||
await render(executeWorkflow(id, initialInput, interactive));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "resume": {
|
|
||||||
const resumeInput = message !== "" ? message : (interactive ? await promptForResumeInput() : "");
|
|
||||||
await render(resumeWorkflow(id, resumeInput, interactive));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error("Failed:", err instanceof Error ? err.message : String(err));
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
20
apps/cli/src/application/assistant/agent.ts
Normal file
20
apps/cli/src/application/assistant/agent.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Agent, ToolAttachment } from "../entities/agent.js";
|
||||||
|
import z from "zod";
|
||||||
|
import { CopilotInstructions } from "./instructions.js";
|
||||||
|
import { BuiltinTools } from "../lib/builtin-tools.js";
|
||||||
|
|
||||||
|
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
|
||||||
|
for (const [name, tool] of Object.entries(BuiltinTools)) {
|
||||||
|
tools[name] = {
|
||||||
|
type: "builtin",
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopilotAgent: z.infer<typeof Agent> = {
|
||||||
|
name: "rowboatx",
|
||||||
|
description: "Rowboatx copilot",
|
||||||
|
instructions: CopilotInstructions,
|
||||||
|
model: "gpt-4.1",
|
||||||
|
tools,
|
||||||
|
}
|
||||||
|
|
@ -1,731 +0,0 @@
|
||||||
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
|
|
||||||
import * as readline from "readline/promises";
|
|
||||||
import { stdin as input, stdout as output } from "process";
|
|
||||||
import { z } from "zod";
|
|
||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as os from "os";
|
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
||||||
import { StreamRenderer } from "../lib/stream-renderer.js";
|
|
||||||
import { getProvider } from "../lib/models.js";
|
|
||||||
import { ModelConfig } from "../config/config.js";
|
|
||||||
import { executeCommand } from "../lib/command-executor.js";
|
|
||||||
|
|
||||||
const rl = readline.createInterface({ input, output });
|
|
||||||
|
|
||||||
// Base directory for file operations - dynamically use user's home directory
|
|
||||||
const BASE_DIR = path.join(os.homedir(), ".rowboat");
|
|
||||||
|
|
||||||
// Ensure base directory exists
|
|
||||||
async function ensureBaseDir() {
|
|
||||||
try {
|
|
||||||
await fs.access(BASE_DIR);
|
|
||||||
} catch {
|
|
||||||
await fs.mkdir(BASE_DIR, { recursive: true });
|
|
||||||
console.log(`📁 Created directory: ${BASE_DIR}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export the main copilot function
|
|
||||||
export async function startCopilot() {
|
|
||||||
// Conversation history
|
|
||||||
const messages: ModelMessage[] = [];
|
|
||||||
|
|
||||||
console.log("🤖 Rowboat Copilot - Your Intelligent Workflow Assistant");
|
|
||||||
console.log(`📂 Working directory: ${BASE_DIR}`);
|
|
||||||
console.log("💡 I can help you create, manage, and understand workflows.");
|
|
||||||
console.log("Type 'exit' to quit\n");
|
|
||||||
|
|
||||||
// Initialize base directory
|
|
||||||
await ensureBaseDir();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
// Get user input
|
|
||||||
const userInput = await rl.question("You: ");
|
|
||||||
|
|
||||||
// Exit condition
|
|
||||||
if (userInput.toLowerCase() === "exit" || userInput.toLowerCase() === "quit") {
|
|
||||||
console.log("\n👋 Goodbye!");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user message to history
|
|
||||||
messages.push({ role: "user", content: userInput });
|
|
||||||
|
|
||||||
// Stream AI response
|
|
||||||
process.stdout.write("\nCopilot: ");
|
|
||||||
|
|
||||||
let currentStep = 0;
|
|
||||||
const provider = getProvider();
|
|
||||||
const result = streamText({
|
|
||||||
model: provider(ModelConfig.defaults.model),
|
|
||||||
messages: messages,
|
|
||||||
system: `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}.
|
|
||||||
|
|
||||||
WORKFLOW KNOWLEDGE:
|
|
||||||
- Workflows are JSON files that orchestrate multiple agents
|
|
||||||
- Agents are JSON files defining AI assistants with specific tools and instructions
|
|
||||||
- Tools can be built-in functions or MCP (Model Context Protocol) integrations
|
|
||||||
|
|
||||||
NOTE: Comments with // in the formats below are for explanation only - do NOT include them in actual JSON files
|
|
||||||
|
|
||||||
CORRECT WORKFLOW FORMAT:
|
|
||||||
{
|
|
||||||
"name": "workflow_name", // REQUIRED - must match filename
|
|
||||||
"description": "Description...", // REQUIRED - must be a description of the workflow
|
|
||||||
"steps": [ // REQUIRED - array of steps
|
|
||||||
{
|
|
||||||
"type": "agent", // REQUIRED - always "agent"
|
|
||||||
"id": "agent_name" // REQUIRED - must match agent filename
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "agent",
|
|
||||||
"id": "another_agent_name"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
CORRECT AGENT FORMAT (with detailed tool structure):
|
|
||||||
{
|
|
||||||
"name": "agent_name", // REQUIRED - must match filename
|
|
||||||
"description": "What agent does", // REQUIRED - must be a description of the agent
|
|
||||||
"model": "gpt-4.1", // REQUIRED - model to use
|
|
||||||
"instructions": "Instructions...", // REQUIRED - agent instructions
|
|
||||||
"tools": { // OPTIONAL - can be empty {} or omitted
|
|
||||||
"descriptive_tool_name": {
|
|
||||||
"type": "mcp", // REQUIRED - always "mcp" for MCP tools
|
|
||||||
"name": "actual_mcp_tool_name", // REQUIRED - exact tool name from MCP server
|
|
||||||
"description": "What tool does", // REQUIRED - clear description
|
|
||||||
"mcpServerName": "server_name", // REQUIRED - name from mcp.json config
|
|
||||||
"inputSchema": { // REQUIRED - full JSON schema
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"param1": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Description of param" // description is optional but helpful
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["param1"] // OPTIONAL - only include if params are required
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IMPORTANT NOTES:
|
|
||||||
- Agent tools need: type, name, description, mcpServerName, and inputSchema (all REQUIRED)
|
|
||||||
- Tool keys in agents should be descriptive (like "search", "fetch", "analyze") not the exact tool name
|
|
||||||
- Agents can have empty tools {} if they don't need external tools
|
|
||||||
- The "required" array in inputSchema is OPTIONAL - only include it if the tool has required parameters
|
|
||||||
- If all parameters are optional, you can omit the "required" field entirely
|
|
||||||
- Property descriptions in inputSchema are optional but helpful for clarity
|
|
||||||
- All other fields marked REQUIRED must always be present
|
|
||||||
|
|
||||||
EXAMPLE 1 - Firecrawl Search Tool (with required params):
|
|
||||||
{
|
|
||||||
"tools": {
|
|
||||||
"search": {
|
|
||||||
"type": "mcp",
|
|
||||||
"name": "firecrawl_search",
|
|
||||||
"description": "Search the web",
|
|
||||||
"mcpServerName": "firecrawl",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"query": {"type": "string", "description": "Search query"},
|
|
||||||
"limit": {"type": "number", "description": "Number of results"},
|
|
||||||
"sources": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {"type": "string", "enum": ["web", "images", "news"]}
|
|
||||||
},
|
|
||||||
"required": ["type"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["query"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EXAMPLE 2 - ElevenLabs Text-to-Speech (without required array):
|
|
||||||
{
|
|
||||||
"tools": {
|
|
||||||
"text_to_speech": {
|
|
||||||
"type": "mcp",
|
|
||||||
"name": "text_to_speech",
|
|
||||||
"description": "Generate audio from text",
|
|
||||||
"mcpServerName": "elevenLabs",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"text": {"type": "string"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CRITICAL NAMING AND ORGANIZATION RULES:
|
|
||||||
- Agent filenames MUST match the "name" field in their JSON (e.g., agent_name.json → "name": "agent_name")
|
|
||||||
- Workflow filenames MUST match the "name" field in their JSON (e.g., workflow_name.json → "name": "workflow_name")
|
|
||||||
- When referencing agents in workflow steps, the "id" field MUST match the agent's name (e.g., {"type": "agent", "id": "agent_name"})
|
|
||||||
- All three must be identical: filename, JSON "name" field, and workflow step "id" field
|
|
||||||
- ALL workflows MUST be placed in the "workflows/" folder (e.g., workflows/workflow_name.json)
|
|
||||||
- ALL agents MUST be placed in the "agents/" folder (e.g., agents/agent_name.json)
|
|
||||||
- NEVER create workflows or agents outside these designated folders
|
|
||||||
- Always maintain this naming and organizational consistency when creating or updating files
|
|
||||||
|
|
||||||
YOUR CAPABILITIES:
|
|
||||||
1. Explore the directory structure to understand existing workflows/agents
|
|
||||||
2. Create new workflows and agents following best practices
|
|
||||||
3. Update existing files intelligently
|
|
||||||
4. Read and analyze file contents to maintain consistency
|
|
||||||
5. Suggest improvements and ask clarifying questions when needed
|
|
||||||
6. Execute shell commands to perform system operations
|
|
||||||
- Use executeCommand to run bash/shell commands
|
|
||||||
- Can list files, check system info, run scripts, etc.
|
|
||||||
- Commands execute in the .rowboat directory by default
|
|
||||||
7. List and explore MCP (Model Context Protocol) servers and their available tools
|
|
||||||
- Use listMcpServers to see all configured MCP servers
|
|
||||||
- Use listMcpTools to see what tools are available in a specific MCP server
|
|
||||||
- This helps users understand what external integrations they can use in their workflows
|
|
||||||
|
|
||||||
MCP INTEGRATION:
|
|
||||||
- MCP servers provide external tools that agents can use (e.g., web scraping, database access, APIs)
|
|
||||||
- MCP configuration is stored in config/mcp.json
|
|
||||||
- When users ask about available integrations or tools, check MCP servers
|
|
||||||
- Help users understand which MCP tools they can add to their agents
|
|
||||||
|
|
||||||
DELETION RULES:
|
|
||||||
- When a user asks to delete a WORKFLOW, you MUST:
|
|
||||||
1. First read/analyze the workflow to identify which agents it uses
|
|
||||||
2. List those agents to the user
|
|
||||||
3. Ask the user if they want to delete those agents as well
|
|
||||||
4. Wait for their response before proceeding with any deletions
|
|
||||||
5. Only delete what the user confirms
|
|
||||||
- When a user asks to delete an AGENT, you MUST:
|
|
||||||
1. First read/analyze the agent to identify which workflows it is used in
|
|
||||||
2. List those workflows to the user
|
|
||||||
3. Ask the user if they want to delete/modify those workflows as well
|
|
||||||
4. Wait for their response before proceeding with any deletions
|
|
||||||
5. Only delete/modify what the user confirms
|
|
||||||
|
|
||||||
COMMUNICATION STYLE:
|
|
||||||
- Break down complex tasks into clear steps
|
|
||||||
- Explore existing files/structure before creating new ones
|
|
||||||
- Explain your reasoning as you work through tasks
|
|
||||||
- Be proactive in understanding context
|
|
||||||
- Confirm what you've done and suggest next steps
|
|
||||||
- Always ask for confirmation before destructive operations!!
|
|
||||||
|
|
||||||
Always use relative paths (no ${BASE_DIR} prefix) when calling tools.`,
|
|
||||||
|
|
||||||
tools: {
|
|
||||||
exploreDirectory: tool({
|
|
||||||
description: 'Recursively explore directory structure to understand existing workflows, agents, and file organization',
|
|
||||||
inputSchema: z.object({
|
|
||||||
subdirectory: z.string().optional().describe('Subdirectory to explore (optional, defaults to root)'),
|
|
||||||
maxDepth: z.number().optional().describe('Maximum depth to traverse (default: 3)'),
|
|
||||||
}),
|
|
||||||
execute: async ({ subdirectory, maxDepth = 3 }) => {
|
|
||||||
async function explore(dir: string, depth: number = 0): Promise<any> {
|
|
||||||
if (depth > maxDepth) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
const result: any = { files: [], directories: {} };
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isFile()) {
|
|
||||||
const ext = path.extname(entry.name);
|
|
||||||
const size = (await fs.stat(fullPath)).size;
|
|
||||||
result.files.push({
|
|
||||||
name: entry.name,
|
|
||||||
type: ext || 'no-extension',
|
|
||||||
size: size,
|
|
||||||
relativePath: path.relative(BASE_DIR, fullPath),
|
|
||||||
});
|
|
||||||
} else if (entry.isDirectory()) {
|
|
||||||
result.directories[entry.name] = await explore(fullPath, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirPath = subdirectory ? path.join(BASE_DIR, subdirectory) : BASE_DIR;
|
|
||||||
const structure = await explore(dirPath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
basePath: path.relative(BASE_DIR, dirPath) || '.',
|
|
||||||
structure,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
readFile: tool({
|
|
||||||
description: 'Read and parse file contents. For JSON files, provides parsed structure.',
|
|
||||||
inputSchema: z.object({
|
|
||||||
filename: z.string().describe('The name of the file to read (relative to .rowboat directory)'),
|
|
||||||
}),
|
|
||||||
execute: async ({ filename }) => {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(BASE_DIR, filename);
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
|
||||||
|
|
||||||
let parsed = null;
|
|
||||||
let fileType = path.extname(filename);
|
|
||||||
|
|
||||||
if (fileType === '.json') {
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(content);
|
|
||||||
} catch {
|
|
||||||
parsed = { error: 'Invalid JSON' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
filename,
|
|
||||||
fileType,
|
|
||||||
content,
|
|
||||||
parsed,
|
|
||||||
path: filePath,
|
|
||||||
size: content.length,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
createFile: tool({
|
|
||||||
description: 'Create a new file with content. Automatically creates parent directories if needed.',
|
|
||||||
inputSchema: z.object({
|
|
||||||
filename: z.string().describe('The name of the file to create (relative to .rowboat directory)'),
|
|
||||||
content: z.string().describe('The content to write to the file'),
|
|
||||||
description: z.string().optional().describe('Optional description of why this file is being created'),
|
|
||||||
}),
|
|
||||||
execute: async ({ filename, content, description }) => {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(BASE_DIR, filename);
|
|
||||||
const dir = path.dirname(filePath);
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
await fs.mkdir(dir, { recursive: true });
|
|
||||||
|
|
||||||
// Write file
|
|
||||||
await fs.writeFile(filePath, content, 'utf-8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `File '${filename}' created successfully`,
|
|
||||||
description: description || 'No description provided',
|
|
||||||
path: filePath,
|
|
||||||
size: content.length,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to create file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateFile: tool({
|
|
||||||
description: 'Update or overwrite the contents of an existing file',
|
|
||||||
inputSchema: z.object({
|
|
||||||
filename: z.string().describe('The name of the file to update (relative to .rowboat directory)'),
|
|
||||||
content: z.string().describe('The new content to write to the file'),
|
|
||||||
reason: z.string().optional().describe('Optional reason for the update'),
|
|
||||||
}),
|
|
||||||
execute: async ({ filename, content, reason }) => {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(BASE_DIR, filename);
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
await fs.access(filePath);
|
|
||||||
|
|
||||||
// Update file
|
|
||||||
await fs.writeFile(filePath, content, 'utf-8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `File '${filename}' updated successfully`,
|
|
||||||
reason: reason || 'No reason provided',
|
|
||||||
path: filePath,
|
|
||||||
size: content.length,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to update file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteFile: tool({
|
|
||||||
description: 'Delete a file from the .rowboat directory',
|
|
||||||
inputSchema: z.object({
|
|
||||||
filename: z.string().describe('The name of the file to delete (relative to .rowboat directory)'),
|
|
||||||
}),
|
|
||||||
execute: async ({ filename }) => {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(BASE_DIR, filename);
|
|
||||||
await fs.unlink(filePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `File '${filename}' deleted successfully`,
|
|
||||||
path: filePath,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
listFiles: tool({
|
|
||||||
description: 'List all files and directories in the .rowboat directory or subdirectory',
|
|
||||||
inputSchema: z.object({
|
|
||||||
subdirectory: z.string().optional().describe('Optional subdirectory to list (relative to .rowboat directory)'),
|
|
||||||
}),
|
|
||||||
execute: async ({ subdirectory }) => {
|
|
||||||
try {
|
|
||||||
const dirPath = subdirectory ? path.join(BASE_DIR, subdirectory) : BASE_DIR;
|
|
||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
const files = entries
|
|
||||||
.filter(entry => entry.isFile())
|
|
||||||
.map(entry => ({
|
|
||||||
name: entry.name,
|
|
||||||
type: path.extname(entry.name) || 'no-extension',
|
|
||||||
relativePath: path.relative(BASE_DIR, path.join(dirPath, entry.name)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const directories = entries
|
|
||||||
.filter(entry => entry.isDirectory())
|
|
||||||
.map(entry => entry.name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
path: dirPath,
|
|
||||||
relativePath: path.relative(BASE_DIR, dirPath) || '.',
|
|
||||||
files,
|
|
||||||
directories,
|
|
||||||
totalFiles: files.length,
|
|
||||||
totalDirectories: directories.length,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to list files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
analyzeWorkflow: tool({
|
|
||||||
description: 'Read and analyze a workflow file to understand its structure, agents, and dependencies',
|
|
||||||
inputSchema: z.object({
|
|
||||||
workflowName: z.string().describe('Name of the workflow file to analyze (with or without .json extension)'),
|
|
||||||
}),
|
|
||||||
execute: async ({ workflowName }) => {
|
|
||||||
try {
|
|
||||||
const filename = workflowName.endsWith('.json') ? workflowName : `${workflowName}.json`;
|
|
||||||
const filePath = path.join(BASE_DIR, 'workflows', filename);
|
|
||||||
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const workflow = JSON.parse(content);
|
|
||||||
|
|
||||||
// Extract key information
|
|
||||||
const analysis = {
|
|
||||||
name: workflow.name,
|
|
||||||
description: workflow.description || 'No description',
|
|
||||||
agentCount: workflow.agents ? workflow.agents.length : 0,
|
|
||||||
agents: workflow.agents || [],
|
|
||||||
tools: workflow.tools || {},
|
|
||||||
structure: workflow,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
filePath: path.relative(BASE_DIR, filePath),
|
|
||||||
analysis,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to analyze workflow: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
listMcpServers: tool({
|
|
||||||
description: 'List all available MCP servers from the configuration',
|
|
||||||
inputSchema: z.object({}),
|
|
||||||
execute: async () => {
|
|
||||||
try {
|
|
||||||
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
|
||||||
|
|
||||||
// Check if config exists
|
|
||||||
try {
|
|
||||||
await fs.access(configPath);
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
servers: [],
|
|
||||||
message: 'No MCP servers configured yet',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.readFile(configPath, 'utf-8');
|
|
||||||
const config = JSON.parse(content);
|
|
||||||
|
|
||||||
const servers = Object.keys(config.mcpServers || {}).map(name => {
|
|
||||||
const server = config.mcpServers[name];
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
type: 'command' in server ? 'stdio' : 'http',
|
|
||||||
command: server.command,
|
|
||||||
url: server.url,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
servers,
|
|
||||||
count: servers.length,
|
|
||||||
message: `Found ${servers.length} MCP server(s)`,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
listMcpTools: tool({
|
|
||||||
description: 'List all available tools from a specific MCP server',
|
|
||||||
inputSchema: z.object({
|
|
||||||
serverName: z.string().describe('Name of the MCP server to query'),
|
|
||||||
}),
|
|
||||||
execute: async ({ serverName }) => {
|
|
||||||
try {
|
|
||||||
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
|
||||||
const content = await fs.readFile(configPath, 'utf-8');
|
|
||||||
const config = JSON.parse(content);
|
|
||||||
|
|
||||||
const mcpConfig = config.mcpServers[serverName];
|
|
||||||
if (!mcpConfig) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `MCP server '${serverName}' not found in configuration`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create transport based on config type
|
|
||||||
let transport;
|
|
||||||
if ('command' in mcpConfig) {
|
|
||||||
transport = new StdioClientTransport({
|
|
||||||
command: mcpConfig.command,
|
|
||||||
args: mcpConfig.args || [],
|
|
||||||
env: mcpConfig.env || {},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
|
|
||||||
} catch {
|
|
||||||
transport = new SSEClientTransport(new URL(mcpConfig.url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and connect client
|
|
||||||
const client = new Client({
|
|
||||||
name: 'rowboat-copilot',
|
|
||||||
version: '1.0.0',
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect(transport);
|
|
||||||
|
|
||||||
// List available tools
|
|
||||||
const toolsList = await client.listTools();
|
|
||||||
|
|
||||||
// Close connection
|
|
||||||
client.close();
|
|
||||||
transport.close();
|
|
||||||
|
|
||||||
const tools = toolsList.tools.map((t: any) => ({
|
|
||||||
name: t.name,
|
|
||||||
description: t.description || 'No description',
|
|
||||||
inputSchema: t.inputSchema,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
serverName,
|
|
||||||
tools,
|
|
||||||
count: tools.length,
|
|
||||||
message: `Found ${tools.length} tool(s) in MCP server '${serverName}'`,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
executeCommand: tool({
|
|
||||||
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
|
|
||||||
inputSchema: z.object({
|
|
||||||
command: z.string().describe('The shell command to execute (e.g., "ls -la", "cat file.txt")'),
|
|
||||||
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to .rowboat directory)'),
|
|
||||||
}),
|
|
||||||
execute: async ({ command, cwd }) => {
|
|
||||||
try {
|
|
||||||
const workingDir = cwd ? path.join(BASE_DIR, cwd) : BASE_DIR;
|
|
||||||
const result = await executeCommand(command, { cwd: workingDir });
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: result.exitCode === 0,
|
|
||||||
stdout: result.stdout,
|
|
||||||
stderr: result.stderr,
|
|
||||||
exitCode: result.exitCode,
|
|
||||||
command,
|
|
||||||
workingDir,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to execute command: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
command,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
stopWhen: stepCountIs(20),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize renderer with workflow-style output
|
|
||||||
const renderer = new StreamRenderer({
|
|
||||||
showHeaders: false,
|
|
||||||
dimReasoning: true,
|
|
||||||
jsonIndent: 2,
|
|
||||||
truncateJsonAt: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stream and collect response using fullStream
|
|
||||||
let assistantResponse = "";
|
|
||||||
const { fullStream } = result;
|
|
||||||
|
|
||||||
for await (const event of fullStream) {
|
|
||||||
switch (event.type) {
|
|
||||||
case "reasoning-start":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: { type: "reasoning-start" }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "reasoning-delta":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: { type: "reasoning-delta", delta: event.text }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "reasoning-end":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: { type: "reasoning-end" }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "text-start":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: { type: "text-start" }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "text-delta":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: { type: "text-delta", delta: event.text }
|
|
||||||
});
|
|
||||||
assistantResponse += event.text;
|
|
||||||
break;
|
|
||||||
case "text-end":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: { type: "text-end" }
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "tool-call":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: {
|
|
||||||
type: "tool-call",
|
|
||||||
toolCallId: event.toolCallId,
|
|
||||||
toolName: event.toolName,
|
|
||||||
input: 'args' in event ? event.args : event.input
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "tool-result":
|
|
||||||
// Tool results are not directly rendered in copilot mode
|
|
||||||
break;
|
|
||||||
case "finish":
|
|
||||||
renderer.render({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: "copilot",
|
|
||||||
event: {
|
|
||||||
type: "usage",
|
|
||||||
usage: event.totalUsage
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// Add assistant response to history
|
|
||||||
messages.push({ role: "assistant", content: assistantResponse });
|
|
||||||
|
|
||||||
// Keep only the last 20 messages (10 user + 10 assistant pairs)
|
|
||||||
if (messages.length > 20) {
|
|
||||||
messages.splice(0, messages.length - 20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
164
apps/cli/src/application/assistant/instructions.ts
Normal file
164
apps/cli/src/application/assistant/instructions.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { WorkDir as BASE_DIR } from "../config/config.js";
|
||||||
|
|
||||||
|
export const CopilotInstructions = `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}.
|
||||||
|
|
||||||
|
WORKFLOW KNOWLEDGE:
|
||||||
|
- Workflows are JSON files that orchestrate multiple agents
|
||||||
|
- Agents are JSON files defining AI assistants with specific tools and instructions
|
||||||
|
- Tools can be built-in functions or MCP (Model Context Protocol) integrations
|
||||||
|
|
||||||
|
NOTE: Comments with // in the formats below are for explanation only - do NOT include them in actual JSON files
|
||||||
|
|
||||||
|
CORRECT WORKFLOW FORMAT:
|
||||||
|
{
|
||||||
|
"name": "workflow_name", // REQUIRED - must match filename
|
||||||
|
"description": "Description...", // REQUIRED - must be a description of the workflow
|
||||||
|
"steps": [ // REQUIRED - array of steps
|
||||||
|
{
|
||||||
|
"type": "agent", // REQUIRED - always "agent"
|
||||||
|
"id": "agent_name" // REQUIRED - must match agent filename
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "agent",
|
||||||
|
"id": "another_agent_name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
CORRECT AGENT FORMAT (with detailed tool structure):
|
||||||
|
{
|
||||||
|
"name": "agent_name", // REQUIRED - must match filename
|
||||||
|
"description": "What agent does", // REQUIRED - must be a description of the agent
|
||||||
|
"model": "gpt-4.1", // REQUIRED - model to use
|
||||||
|
"instructions": "Instructions...", // REQUIRED - agent instructions
|
||||||
|
"tools": { // OPTIONAL - can be empty {} or omitted
|
||||||
|
"descriptive_tool_name": {
|
||||||
|
"type": "mcp", // REQUIRED - always "mcp" for MCP tools
|
||||||
|
"name": "actual_mcp_tool_name", // REQUIRED - exact tool name from MCP server
|
||||||
|
"description": "What tool does", // REQUIRED - clear description
|
||||||
|
"mcpServerName": "server_name", // REQUIRED - name from mcp.json config
|
||||||
|
"inputSchema": { // REQUIRED - full JSON schema
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"param1": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of param" // description is optional but helpful
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["param1"] // OPTIONAL - only include if params are required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPORTANT NOTES:
|
||||||
|
- Agent tools need: type, name, description, mcpServerName, and inputSchema (all REQUIRED)
|
||||||
|
- Tool keys in agents should be descriptive (like "search", "fetch", "analyze") not the exact tool name
|
||||||
|
- Agents can have empty tools {} if they don't need external tools
|
||||||
|
- The "required" array in inputSchema is OPTIONAL - only include it if the tool has required parameters
|
||||||
|
- If all parameters are optional, you can omit the "required" field entirely
|
||||||
|
- Property descriptions in inputSchema are optional but helpful for clarity
|
||||||
|
- All other fields marked REQUIRED must always be present
|
||||||
|
|
||||||
|
EXAMPLE 1 - Firecrawl Search Tool (with required params):
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"search": {
|
||||||
|
"type": "mcp",
|
||||||
|
"name": "firecrawl_search",
|
||||||
|
"description": "Search the web",
|
||||||
|
"mcpServerName": "firecrawl",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string", "description": "Search query"},
|
||||||
|
"limit": {"type": "number", "description": "Number of results"},
|
||||||
|
"sources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"type": "string", "enum": ["web", "images", "news"]}
|
||||||
|
},
|
||||||
|
"required": ["type"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EXAMPLE 2 - ElevenLabs Text-to-Speech (without required array):
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"text_to_speech": {
|
||||||
|
"type": "mcp",
|
||||||
|
"name": "text_to_speech",
|
||||||
|
"description": "Generate audio from text",
|
||||||
|
"mcpServerName": "elevenLabs",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CRITICAL NAMING AND ORGANIZATION RULES:
|
||||||
|
- Agent filenames MUST match the "name" field in their JSON (e.g., agent_name.json → "name": "agent_name")
|
||||||
|
- Workflow filenames MUST match the "name" field in their JSON (e.g., workflow_name.json → "name": "workflow_name")
|
||||||
|
- When referencing agents in workflow steps, the "id" field MUST match the agent's name (e.g., {"type": "agent", "id": "agent_name"})
|
||||||
|
- All three must be identical: filename, JSON "name" field, and workflow step "id" field
|
||||||
|
- ALL workflows MUST be placed in the "workflows/" folder (e.g., workflows/workflow_name.json)
|
||||||
|
- ALL agents MUST be placed in the "agents/" folder (e.g., agents/agent_name.json)
|
||||||
|
- NEVER create workflows or agents outside these designated folders
|
||||||
|
- Always maintain this naming and organizational consistency when creating or updating files
|
||||||
|
|
||||||
|
YOUR CAPABILITIES:
|
||||||
|
1. Explore the directory structure to understand existing workflows/agents
|
||||||
|
2. Create new workflows and agents following best practices
|
||||||
|
3. Update existing files intelligently
|
||||||
|
4. Read and analyze file contents to maintain consistency
|
||||||
|
5. Suggest improvements and ask clarifying questions when needed
|
||||||
|
6. Execute shell commands to perform system operations
|
||||||
|
- Use executeCommand to run bash/shell commands
|
||||||
|
- Can list files, check system info, run scripts, etc.
|
||||||
|
- Commands execute in the .rowboat directory by default
|
||||||
|
7. List and explore MCP (Model Context Protocol) servers and their available tools
|
||||||
|
- Use listMcpServers to see all configured MCP servers
|
||||||
|
- Use listMcpTools to see what tools are available in a specific MCP server
|
||||||
|
- This helps users understand what external integrations they can use in their workflows
|
||||||
|
|
||||||
|
MCP INTEGRATION:
|
||||||
|
- MCP servers provide external tools that agents can use (e.g., web scraping, database access, APIs)
|
||||||
|
- MCP configuration is stored in config/mcp.json
|
||||||
|
- When users ask about available integrations or tools, check MCP servers
|
||||||
|
- Help users understand which MCP tools they can add to their agents
|
||||||
|
|
||||||
|
DELETION RULES:
|
||||||
|
- When a user asks to delete a WORKFLOW, you MUST:
|
||||||
|
1. First read/analyze the workflow to identify which agents it uses
|
||||||
|
2. List those agents to the user
|
||||||
|
3. Ask the user if they want to delete those agents as well
|
||||||
|
4. Wait for their response before proceeding with any deletions
|
||||||
|
5. Only delete what the user confirms
|
||||||
|
- When a user asks to delete an AGENT, you MUST:
|
||||||
|
1. First read/analyze the agent to identify which workflows it is used in
|
||||||
|
2. List those workflows to the user
|
||||||
|
3. Ask the user if they want to delete/modify those workflows as well
|
||||||
|
4. Wait for their response before proceeding with any deletions
|
||||||
|
5. Only delete/modify what the user confirms
|
||||||
|
|
||||||
|
COMMUNICATION STYLE:
|
||||||
|
- Break down complex tasks into clear steps
|
||||||
|
- Explore existing files/structure before creating new ones
|
||||||
|
- Explain your reasoning as you work through tasks
|
||||||
|
- Be proactive in understanding context
|
||||||
|
- Confirm what you've done and suggest next steps
|
||||||
|
- Always ask for confirmation before destructive operations!!
|
||||||
|
|
||||||
|
Always use relative paths (no ${BASE_DIR} prefix) when calling tools.`;
|
||||||
|
|
@ -34,7 +34,7 @@ const baseModelConfig: z.infer<typeof ModelConfigT> = {
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-4.1",
|
model: "gpt-5",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,7 +55,6 @@ function ensureModelConfig() {
|
||||||
function ensureDirs() {
|
function ensureDirs() {
|
||||||
const ensure = (p: string) => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); };
|
const ensure = (p: string) => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); };
|
||||||
ensure(WorkDir);
|
ensure(WorkDir);
|
||||||
ensure(path.join(WorkDir, "workflows"));
|
|
||||||
ensure(path.join(WorkDir, "agents"));
|
ensure(path.join(WorkDir, "agents"));
|
||||||
ensure(path.join(WorkDir, "config"));
|
ensure(path.join(WorkDir, "config"));
|
||||||
ensureMcpConfig();
|
ensureMcpConfig();
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const BaseAgentTool = z.object({
|
export const BaseTool = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BuiltinAgentTool = BaseAgentTool.extend({
|
export const BuiltinTool = BaseTool.extend({
|
||||||
type: z.literal("builtin"),
|
type: z.literal("builtin"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const McpAgentTool = BaseAgentTool.extend({
|
export const McpTool = BaseTool.extend({
|
||||||
type: z.literal("mcp"),
|
type: z.literal("mcp"),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
inputSchema: z.any(),
|
inputSchema: z.any(),
|
||||||
mcpServerName: z.string(),
|
mcpServerName: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WorkflowAgentTool = BaseAgentTool.extend({
|
export const AgentAsATool = BaseTool.extend({
|
||||||
type: z.literal("workflow"),
|
type: z.literal("agent"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AgentTool = z.discriminatedUnion("type", [
|
export const ToolAttachment = z.discriminatedUnion("type", [
|
||||||
BuiltinAgentTool,
|
BuiltinTool,
|
||||||
McpAgentTool,
|
McpTool,
|
||||||
WorkflowAgentTool,
|
AgentAsATool,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Agent = z.object({
|
export const Agent = z.object({
|
||||||
|
|
@ -31,5 +31,5 @@ export const Agent = z.object({
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
instructions: z.string(),
|
instructions: z.string(),
|
||||||
tools: z.record(z.string(), AgentTool).optional(),
|
tools: z.record(z.string(), ToolAttachment).optional(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { LlmStepStreamEvent } from "./llm-step-event.js";
|
import { LlmStepStreamEvent } from "./llm-step-events.js";
|
||||||
import { Workflow } from "./workflow.js";
|
|
||||||
import { Message } from "./message.js";
|
import { Message } from "./message.js";
|
||||||
|
import { Agent } from "./agent.js";
|
||||||
|
|
||||||
const BaseRunEvent = z.object({
|
const BaseRunEvent = z.object({
|
||||||
ts: z.iso.datetime().optional(),
|
ts: z.iso.datetime().optional(),
|
||||||
|
|
@ -10,47 +10,39 @@ const BaseRunEvent = z.object({
|
||||||
export const RunStartEvent = BaseRunEvent.extend({
|
export const RunStartEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("start"),
|
type: z.literal("start"),
|
||||||
runId: z.string(),
|
runId: z.string(),
|
||||||
workflowId: z.string(),
|
agentId: z.string(),
|
||||||
workflow: Workflow,
|
agent: Agent,
|
||||||
interactive: z.boolean(),
|
interactive: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunStepStartEvent = BaseRunEvent.extend({
|
export const RunStepStartEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("step-start"),
|
type: z.literal("step-start"),
|
||||||
stepIndex: z.number(),
|
|
||||||
stepId: z.string(),
|
|
||||||
stepType: z.enum(["agent", "function"]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunStreamEvent = BaseRunEvent.extend({
|
export const RunStreamEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("stream-event"),
|
type: z.literal("stream-event"),
|
||||||
stepId: z.string(),
|
|
||||||
event: LlmStepStreamEvent,
|
event: LlmStepStreamEvent,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunMessageEvent = BaseRunEvent.extend({
|
export const RunMessageEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("message"),
|
type: z.literal("message"),
|
||||||
stepId: z.string(),
|
|
||||||
message: Message,
|
message: Message,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunToolInvocationEvent = BaseRunEvent.extend({
|
export const RunToolInvocationEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("tool-invocation"),
|
type: z.literal("tool-invocation"),
|
||||||
stepId: z.string(),
|
|
||||||
toolName: z.string(),
|
toolName: z.string(),
|
||||||
input: z.string(),
|
input: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunToolResultEvent = BaseRunEvent.extend({
|
export const RunToolResultEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("tool-result"),
|
type: z.literal("tool-result"),
|
||||||
stepId: z.string(),
|
|
||||||
toolName: z.string(),
|
toolName: z.string(),
|
||||||
result: z.any(),
|
result: z.any(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunStepEndEvent = BaseRunEvent.extend({
|
export const RunStepEndEvent = BaseRunEvent.extend({
|
||||||
type: z.literal("step-end"),
|
type: z.literal("step-end"),
|
||||||
stepIndex: z.number(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RunEndEvent = BaseRunEvent.extend({
|
export const RunEndEvent = BaseRunEvent.extend({
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AgentStep = z.object({
|
|
||||||
type: z.literal("agent"),
|
|
||||||
id: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const FunctionStep = z.object({
|
|
||||||
type: z.literal("function"),
|
|
||||||
id: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Step = z.discriminatedUnion("type", [AgentStep, FunctionStep]);
|
|
||||||
|
|
||||||
export const Workflow = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
steps: z.array(Step),
|
|
||||||
createdAt: z.string().optional(),
|
|
||||||
updatedAt: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { Step, StepOutputT } from "../lib/step.js";
|
|
||||||
import { AgentTool } from "../entities/agent.js";
|
|
||||||
|
|
||||||
export class GetDate implements Step {
|
|
||||||
async* execute(): StepOutputT {
|
|
||||||
yield {
|
|
||||||
type: "text-start",
|
|
||||||
};
|
|
||||||
yield {
|
|
||||||
type: "text-delta",
|
|
||||||
delta: 'The current date is ' + new Date().toISOString(),
|
|
||||||
};
|
|
||||||
yield {
|
|
||||||
type: "text-end",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tools(): Record<string, z.infer<typeof AgentTool>> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +1,22 @@
|
||||||
import { Message, MessageList } from "../entities/message.js";
|
import { jsonSchema, ModelMessage } from "ai";
|
||||||
import { z } from "zod";
|
|
||||||
import { Step, StepInputT, StepOutputT } from "./step.js";
|
|
||||||
import { ModelMessage, stepCountIs, streamText, tool, Tool, ToolSet, jsonSchema } from "ai";
|
|
||||||
import { Agent, AgentTool } from "../entities/agent.js";
|
|
||||||
import { ModelConfig, WorkDir } from "../config/config.js";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { loadWorkflow } from "./utils.js";
|
import { ModelConfig, WorkDir } from "../config/config.js";
|
||||||
|
import { Agent, ToolAttachment } from "../entities/agent.js";
|
||||||
|
import { createInterface, Interface } from "node:readline/promises";
|
||||||
|
import { stdin as input, stdout as output } from "node:process";
|
||||||
|
import { AssistantContentPart, AssistantMessage, Message, MessageList, ToolCallPart, ToolMessage, UserMessage } from "../entities/message.js";
|
||||||
|
import { runIdGenerator } from "./run-id-gen.js";
|
||||||
|
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
|
||||||
|
import { z } from "zod";
|
||||||
import { getProvider } from "./models.js";
|
import { getProvider } from "./models.js";
|
||||||
|
import { LlmStepStreamEvent } from "../entities/llm-step-events.js";
|
||||||
|
import { execTool } from "./exec-tool.js";
|
||||||
|
import { RunEvent } from "../entities/run-events.js";
|
||||||
|
import { CopilotAgent } from "../assistant/agent.js";
|
||||||
|
import { BuiltinTools } from "./builtin-tools.js";
|
||||||
|
|
||||||
const BashTool = tool({
|
export async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<Tool> {
|
||||||
description: "Run a command in the shell",
|
|
||||||
inputSchema: z.object({
|
|
||||||
command: z.string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const AskHumanTool = tool({
|
|
||||||
description: "Ask the human for input",
|
|
||||||
inputSchema: z.object({
|
|
||||||
question: z.string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function mapAgentTool(t: z.infer<typeof AgentTool>): Tool {
|
|
||||||
switch (t.type) {
|
switch (t.type) {
|
||||||
case "mcp":
|
case "mcp":
|
||||||
return tool({
|
return tool({
|
||||||
|
|
@ -31,31 +24,136 @@ function mapAgentTool(t: z.infer<typeof AgentTool>): Tool {
|
||||||
description: t.description,
|
description: t.description,
|
||||||
inputSchema: jsonSchema(t.inputSchema),
|
inputSchema: jsonSchema(t.inputSchema),
|
||||||
});
|
});
|
||||||
case "workflow":
|
case "agent":
|
||||||
const workflow = loadWorkflow(t.name);
|
const agent = await loadAgent(t.name);
|
||||||
if (!workflow) {
|
if (!agent) {
|
||||||
throw new Error(`Workflow ${t.name} not found`);
|
throw new Error(`Agent ${t.name} not found`);
|
||||||
}
|
}
|
||||||
return tool({
|
return tool({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
description: workflow.description,
|
description: agent.description,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
message: z.string().describe("The message to send to the workflow"),
|
message: z.string().describe("The message to send to the workflow"),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
case "builtin":
|
case "builtin":
|
||||||
switch (t.name) {
|
const match = BuiltinTools[t.name];
|
||||||
case "bash":
|
if (!match) {
|
||||||
return BashTool;
|
throw new Error(`Unknown builtin tool: ${t.name}`);
|
||||||
case "ask-human":
|
|
||||||
return AskHumanTool;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown builtin tool: ${t.name}`);
|
|
||||||
}
|
}
|
||||||
|
return tool({
|
||||||
|
description: match.description,
|
||||||
|
inputSchema: match.inputSchema,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
|
export class RunLogger {
|
||||||
|
private logFile: string;
|
||||||
|
private fileHandle: fs.WriteStream;
|
||||||
|
|
||||||
|
ensureRunsDir() {
|
||||||
|
const runsDir = path.join(WorkDir, "runs");
|
||||||
|
if (!fs.existsSync(runsDir)) {
|
||||||
|
fs.mkdirSync(runsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(runId: string) {
|
||||||
|
this.ensureRunsDir();
|
||||||
|
this.logFile = path.join(WorkDir, "runs", `${runId}.jsonl`);
|
||||||
|
this.fileHandle = fs.createWriteStream(this.logFile, {
|
||||||
|
flags: "a",
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(event: z.infer<typeof RunEvent>) {
|
||||||
|
if (event.type !== "stream-event") {
|
||||||
|
this.fileHandle.write(JSON.stringify(event) + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.fileHandle.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogAndYield {
|
||||||
|
private logger: RunLogger
|
||||||
|
|
||||||
|
constructor(logger: RunLogger) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async *logAndYield(event: z.infer<typeof RunEvent>): AsyncGenerator<z.infer<typeof RunEvent>, void, unknown> {
|
||||||
|
const ev = {
|
||||||
|
...event,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
this.logger.log(ev);
|
||||||
|
yield ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StreamStepMessageBuilder {
|
||||||
|
private parts: z.infer<typeof AssistantContentPart>[] = [];
|
||||||
|
private textBuffer: string = "";
|
||||||
|
private reasoningBuffer: string = "";
|
||||||
|
|
||||||
|
flushBuffers() {
|
||||||
|
// skip reasoning
|
||||||
|
// if (this.reasoningBuffer) {
|
||||||
|
// this.parts.push({ type: "reasoning", text: this.reasoningBuffer });
|
||||||
|
// this.reasoningBuffer = "";
|
||||||
|
// }
|
||||||
|
if (this.textBuffer) {
|
||||||
|
this.parts.push({ type: "text", text: this.textBuffer });
|
||||||
|
this.textBuffer = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ingest(event: z.infer<typeof LlmStepStreamEvent>) {
|
||||||
|
switch (event.type) {
|
||||||
|
case "reasoning-start":
|
||||||
|
case "reasoning-end":
|
||||||
|
case "text-start":
|
||||||
|
case "text-end":
|
||||||
|
this.flushBuffers();
|
||||||
|
break;
|
||||||
|
case "reasoning-delta":
|
||||||
|
this.reasoningBuffer += event.delta;
|
||||||
|
break;
|
||||||
|
case "text-delta":
|
||||||
|
this.textBuffer += event.delta;
|
||||||
|
break;
|
||||||
|
case "tool-call":
|
||||||
|
this.parts.push({
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
toolName: event.toolName,
|
||||||
|
arguments: event.input,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): z.infer<typeof AssistantMessage> {
|
||||||
|
this.flushBuffers();
|
||||||
|
return {
|
||||||
|
role: "assistant",
|
||||||
|
content: this.parts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
||||||
|
const agentPath = path.join(WorkDir, "agents", `${id}.json`);
|
||||||
|
const agent = fs.readFileSync(agentPath, "utf8");
|
||||||
|
return Agent.parse(JSON.parse(agent));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
|
||||||
const result: ModelMessage[] = [];
|
const result: ModelMessage[] = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
switch (msg.role) {
|
switch (msg.role) {
|
||||||
|
|
@ -119,100 +217,275 @@ function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AgentNode implements Step {
|
|
||||||
private id: string;
|
|
||||||
private asTool: boolean;
|
|
||||||
private agent: z.infer<typeof Agent>;
|
|
||||||
|
|
||||||
constructor(id: string, asTool: boolean) {
|
export async function* streamAgent(opts: {
|
||||||
this.id = id;
|
agent: string;
|
||||||
this.asTool = asTool;
|
runId?: string;
|
||||||
const agentPath = path.join(WorkDir, "agents", `${id}.json`);
|
input?: string;
|
||||||
const agent = fs.readFileSync(agentPath, "utf8");
|
interactive?: boolean;
|
||||||
this.agent = Agent.parse(JSON.parse(agent));
|
}) {
|
||||||
}
|
const messages: z.infer<typeof MessageList> = [];
|
||||||
|
|
||||||
tools(): Record<string, z.infer<typeof AgentTool>> {
|
// load existing and assemble state if required
|
||||||
return this.agent.tools ?? {};
|
if (opts.runId) {
|
||||||
|
console.error("loading run", opts.runId);
|
||||||
|
let stream: fs.ReadStream | null = null;
|
||||||
|
let rl: Interface | null = null;
|
||||||
|
try {
|
||||||
|
const logFile = path.join(WorkDir, "runs", `${opts.runId}.jsonl`);
|
||||||
|
stream = fs.createReadStream(logFile, { encoding: "utf8" });
|
||||||
|
rl = createInterface({ input: stream, crlfDelay: Infinity });
|
||||||
|
for await (const line of rl) {
|
||||||
|
if (line.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
const event = RunEvent.parse(parsed);
|
||||||
|
switch (event.type) {
|
||||||
|
case "message":
|
||||||
|
messages.push(event.message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stream?.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async* execute(input: StepInputT): StepOutputT {
|
// create runId if not present
|
||||||
// console.log("\n\n\t>>>>\t\tinput", JSON.stringify(input));
|
if (!opts.runId) {
|
||||||
const tools: ToolSet = {};
|
opts.runId = runIdGenerator.next();
|
||||||
// if (!this.background) {
|
}
|
||||||
// tools["ask-human"] = AskHumanTool;
|
|
||||||
// }
|
// load agent data
|
||||||
for (const [name, tool] of Object.entries(this.agent.tools ?? {})) {
|
let agent: z.infer<typeof Agent> | null = null;
|
||||||
if (this.asTool && name === "ask-human") {
|
if (opts.agent === "copilot") {
|
||||||
continue;
|
agent = CopilotAgent;
|
||||||
|
} else {
|
||||||
|
agent = await loadAgent(opts.agent);
|
||||||
|
}
|
||||||
|
if (!agent) {
|
||||||
|
throw new Error("unable to load agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up tools
|
||||||
|
const tools: ToolSet = {};
|
||||||
|
for (const [name, tool] of Object.entries(agent.tools ?? {})) {
|
||||||
|
try {
|
||||||
|
tools[name] = await mapAgentTool(tool);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error mapping tool ${name}:`, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up
|
||||||
|
const logger = new RunLogger(opts.runId);
|
||||||
|
const ly = new LogAndYield(logger);
|
||||||
|
const provider = getProvider(agent.provider);
|
||||||
|
const model = provider(agent.model || ModelConfig.defaults.model);
|
||||||
|
|
||||||
|
// get first input if needed
|
||||||
|
let rl: Interface | null = null;
|
||||||
|
if (opts.interactive) {
|
||||||
|
rl = createInterface({ input, output });
|
||||||
|
}
|
||||||
|
if (opts.input) {
|
||||||
|
const m: z.infer<typeof UserMessage> = {
|
||||||
|
role: "user",
|
||||||
|
content: opts.input,
|
||||||
|
};
|
||||||
|
messages.push(m);
|
||||||
|
yield *ly.logAndYield({
|
||||||
|
type: "message",
|
||||||
|
message: m,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// loop b/w user and agent
|
||||||
|
while (true) {
|
||||||
|
// get input in interactive mode when last message is not user
|
||||||
|
if (opts.interactive && (messages.length === 0 || messages[messages.length - 1].role !== "user")) {
|
||||||
|
const input = await rl!.question("You: ");
|
||||||
|
// Exit condition
|
||||||
|
if (["q", "quit", "exit"].includes(input.toLowerCase())) {
|
||||||
|
console.log("\n👋 Goodbye!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m: z.infer<typeof UserMessage> = {
|
||||||
|
role: "user",
|
||||||
|
content: input,
|
||||||
|
};
|
||||||
|
messages.push(m);
|
||||||
|
yield* ly.logAndYield({
|
||||||
|
type: "message",
|
||||||
|
message: m,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
tools[name] = mapAgentTool(tool);
|
// inner loop to handle tool calls
|
||||||
} catch (error) {
|
while (true) {
|
||||||
console.error(`Error mapping tool ${name}:`, error);
|
// stream agent response and build message
|
||||||
continue;
|
const messageBuilder = new StreamStepMessageBuilder();
|
||||||
|
for await (const event of streamLlm(
|
||||||
|
model,
|
||||||
|
messages,
|
||||||
|
agent.instructions,
|
||||||
|
tools,
|
||||||
|
)) {
|
||||||
|
messageBuilder.ingest(event);
|
||||||
|
yield* ly.logAndYield({
|
||||||
|
type: "stream-event",
|
||||||
|
event: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// build and emit final message from agent response
|
||||||
|
const msg = messageBuilder.get();
|
||||||
|
messages.push(msg);
|
||||||
|
yield* ly.logAndYield({
|
||||||
|
type: "message",
|
||||||
|
message: msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle tool calls
|
||||||
|
const mappedToolCalls: z.infer<typeof MappedToolCall>[] = [];
|
||||||
|
let msgToolCallParts: z.infer<typeof ToolCallPart>[] = [];
|
||||||
|
if (msg.content instanceof Array) {
|
||||||
|
msgToolCallParts = msg.content.filter(part => part.type === "tool-call");
|
||||||
|
}
|
||||||
|
const hasToolCalls = msgToolCallParts.length > 0;
|
||||||
|
console.log(msgToolCallParts);
|
||||||
|
|
||||||
|
// validate and map tool calls
|
||||||
|
for (const part of msgToolCallParts) {
|
||||||
|
const agentTool = tools[part.toolName];
|
||||||
|
if (!agentTool) {
|
||||||
|
throw new Error(`Tool ${part.toolName} not found`);
|
||||||
|
}
|
||||||
|
mappedToolCalls.push({
|
||||||
|
toolCall: part,
|
||||||
|
agentTool: agent.tools![part.toolName],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const call of mappedToolCalls) {
|
||||||
|
const { agentTool, toolCall } = call;
|
||||||
|
yield* ly.logAndYield({
|
||||||
|
type: "tool-invocation",
|
||||||
|
toolName: toolCall.toolName,
|
||||||
|
input: JSON.stringify(toolCall.arguments),
|
||||||
|
});
|
||||||
|
const result = await execTool(agentTool, toolCall.arguments);
|
||||||
|
const resultMsg: z.infer<typeof ToolMessage> = {
|
||||||
|
role: "tool",
|
||||||
|
content: JSON.stringify(result),
|
||||||
|
toolCallId: toolCall.toolCallId,
|
||||||
|
toolName: toolCall.toolName,
|
||||||
|
};
|
||||||
|
messages.push(resultMsg);
|
||||||
|
yield* ly.logAndYield({
|
||||||
|
type: "tool-result",
|
||||||
|
toolName: toolCall.toolName,
|
||||||
|
result: result,
|
||||||
|
});
|
||||||
|
yield* ly.logAndYield({
|
||||||
|
type: "message",
|
||||||
|
message: resultMsg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the agent response had tool calls, replay this agent
|
||||||
|
if (hasToolCalls) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, break
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not interactive, return
|
||||||
|
if (!opts.interactive) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
rl?.close();
|
||||||
|
logger.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// console.log("\n\n\t>>>>\t\ttools", JSON.stringify(tools, null, 2));
|
async function* streamLlm(
|
||||||
|
model: LanguageModel,
|
||||||
const provider = getProvider(this.agent.provider);
|
messages: z.infer<typeof MessageList>,
|
||||||
const { fullStream } = streamText({
|
instructions: string,
|
||||||
model: provider(this.agent.model || ModelConfig.defaults.model),
|
tools: ToolSet,
|
||||||
messages: convertFromMessages(input),
|
): AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, void, unknown> {
|
||||||
system: this.agent.instructions,
|
const { fullStream } = streamText({
|
||||||
stopWhen: stepCountIs(1),
|
model,
|
||||||
tools,
|
messages: convertFromMessages(messages),
|
||||||
});
|
system: instructions,
|
||||||
|
tools,
|
||||||
for await (const event of fullStream) {
|
stopWhen: stepCountIs(1),
|
||||||
// console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event));
|
providerOptions: {
|
||||||
switch (event.type) {
|
openai: {
|
||||||
case "reasoning-start":
|
reasoningEffort: "low",
|
||||||
yield {
|
reasoningSummary: "auto",
|
||||||
type: "reasoning-start",
|
},
|
||||||
};
|
}
|
||||||
break;
|
});
|
||||||
case "reasoning-delta":
|
for await (const event of fullStream) {
|
||||||
yield {
|
// console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event));
|
||||||
type: "reasoning-delta",
|
switch (event.type) {
|
||||||
delta: event.text,
|
case "reasoning-start":
|
||||||
};
|
yield {
|
||||||
break;
|
type: "reasoning-start",
|
||||||
case "reasoning-end":
|
};
|
||||||
yield {
|
break;
|
||||||
type: "reasoning-end",
|
case "reasoning-delta":
|
||||||
};
|
yield {
|
||||||
break;
|
type: "reasoning-delta",
|
||||||
case "text-start":
|
delta: event.text,
|
||||||
yield {
|
};
|
||||||
type: "text-start",
|
break;
|
||||||
};
|
case "reasoning-end":
|
||||||
break;
|
yield {
|
||||||
case "text-delta":
|
type: "reasoning-end",
|
||||||
yield {
|
};
|
||||||
type: "text-delta",
|
break;
|
||||||
delta: event.text,
|
case "text-start":
|
||||||
};
|
yield {
|
||||||
break;
|
type: "text-start",
|
||||||
case "tool-call":
|
};
|
||||||
yield {
|
break;
|
||||||
type: "tool-call",
|
case "text-delta":
|
||||||
toolCallId: event.toolCallId,
|
yield {
|
||||||
toolName: event.toolName,
|
type: "text-delta",
|
||||||
input: event.input,
|
delta: event.text,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "finish":
|
case "tool-call":
|
||||||
yield {
|
yield {
|
||||||
type: "usage",
|
type: "tool-call",
|
||||||
usage: event.totalUsage,
|
toolCallId: event.toolCallId,
|
||||||
};
|
toolName: event.toolName,
|
||||||
break;
|
input: event.input,
|
||||||
default:
|
};
|
||||||
// console.warn("Unknown event type", event);
|
break;
|
||||||
continue;
|
case "finish":
|
||||||
}
|
yield {
|
||||||
|
type: "usage",
|
||||||
|
usage: event.totalUsage,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// console.warn("Unknown event type", event);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const MappedToolCall = z.object({
|
||||||
|
toolCall: ToolCallPart,
|
||||||
|
agentTool: ToolAttachment,
|
||||||
|
});
|
||||||
|
|
|
||||||
424
apps/cli/src/application/lib/builtin-tools.ts
Normal file
424
apps/cli/src/application/lib/builtin-tools.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
import { z, ZodType } from "zod";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import * as path from "path";
|
||||||
|
import { WorkDir as BASE_DIR } from "../config/config.js";
|
||||||
|
import { executeCommand } from "./command-executor.js";
|
||||||
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
||||||
|
|
||||||
|
const BuiltinToolsSchema = z.record(z.string(), z.object({
|
||||||
|
description: z.string(),
|
||||||
|
inputSchema: z.custom<ZodType>(),
|
||||||
|
execute: z.function({
|
||||||
|
input: z.any(),
|
||||||
|
output: z.promise(z.any()),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
|
exploreDirectory: {
|
||||||
|
description: 'Recursively explore directory structure to understand existing workflows, agents, and file organization',
|
||||||
|
inputSchema: z.object({
|
||||||
|
subdirectory: z.string().optional().describe('Subdirectory to explore (optional, defaults to root)'),
|
||||||
|
maxDepth: z.number().optional().describe('Maximum depth to traverse (default: 3)'),
|
||||||
|
}),
|
||||||
|
execute: async ({ subdirectory, maxDepth = 3 }: { subdirectory?: string, maxDepth?: number }) => {
|
||||||
|
async function explore(dir: string, depth: number = 0): Promise<any> {
|
||||||
|
if (depth > maxDepth) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
const result: any = { files: [], directories: {} };
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isFile()) {
|
||||||
|
const ext = path.extname(entry.name);
|
||||||
|
const size = (await fs.stat(fullPath)).size;
|
||||||
|
result.files.push({
|
||||||
|
name: entry.name,
|
||||||
|
type: ext || 'no-extension',
|
||||||
|
size: size,
|
||||||
|
relativePath: path.relative(BASE_DIR, fullPath),
|
||||||
|
});
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
result.directories[entry.name] = await explore(fullPath, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirPath = subdirectory ? path.join(BASE_DIR, subdirectory) : BASE_DIR;
|
||||||
|
const structure = await explore(dirPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
basePath: path.relative(BASE_DIR, dirPath) || '.',
|
||||||
|
structure,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
readFile: {
|
||||||
|
description: 'Read and parse file contents. For JSON files, provides parsed structure.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
filename: z.string().describe('The name of the file to read (relative to .rowboat directory)'),
|
||||||
|
}),
|
||||||
|
execute: async ({ filename }: { filename: string }) => {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(BASE_DIR, filename);
|
||||||
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
|
|
||||||
|
let parsed = null;
|
||||||
|
let fileType = path.extname(filename);
|
||||||
|
|
||||||
|
if (fileType === '.json') {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
parsed = { error: 'Invalid JSON' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
filename,
|
||||||
|
fileType,
|
||||||
|
content,
|
||||||
|
parsed,
|
||||||
|
path: filePath,
|
||||||
|
size: content.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
createFile: {
|
||||||
|
description: 'Create a new file with content. Automatically creates parent directories if needed.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
filename: z.string().describe('The name of the file to create (relative to .rowboat directory)'),
|
||||||
|
content: z.string().describe('The content to write to the file'),
|
||||||
|
description: z.string().optional().describe('Optional description of why this file is being created'),
|
||||||
|
}),
|
||||||
|
execute: async ({ filename, content, description }: { filename: string, content: string, description?: string }) => {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(BASE_DIR, filename);
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
// Write file
|
||||||
|
await fs.writeFile(filePath, content, 'utf-8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `File '${filename}' created successfully`,
|
||||||
|
description: description || 'No description provided',
|
||||||
|
path: filePath,
|
||||||
|
size: content.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to create file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFile: {
|
||||||
|
description: 'Update or overwrite the contents of an existing file',
|
||||||
|
inputSchema: z.object({
|
||||||
|
filename: z.string().describe('The name of the file to update (relative to .rowboat directory)'),
|
||||||
|
content: z.string().describe('The new content to write to the file'),
|
||||||
|
reason: z.string().optional().describe('Optional reason for the update'),
|
||||||
|
}),
|
||||||
|
execute: async ({ filename, content, reason }: { filename: string, content: string, reason?: string }) => {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(BASE_DIR, filename);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
await fs.access(filePath);
|
||||||
|
|
||||||
|
// Update file
|
||||||
|
await fs.writeFile(filePath, content, 'utf-8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `File '${filename}' updated successfully`,
|
||||||
|
reason: reason || 'No reason provided',
|
||||||
|
path: filePath,
|
||||||
|
size: content.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to update file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteFile: {
|
||||||
|
description: 'Delete a file from the .rowboat directory',
|
||||||
|
inputSchema: z.object({
|
||||||
|
filename: z.string().describe('The name of the file to delete (relative to .rowboat directory)'),
|
||||||
|
}),
|
||||||
|
execute: async ({ filename }: { filename: string }) => {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(BASE_DIR, filename);
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `File '${filename}' deleted successfully`,
|
||||||
|
path: filePath,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
listFiles: {
|
||||||
|
description: 'List all files and directories in the .rowboat directory or subdirectory',
|
||||||
|
inputSchema: z.object({
|
||||||
|
subdirectory: z.string().optional().describe('Optional subdirectory to list (relative to .rowboat directory)'),
|
||||||
|
}),
|
||||||
|
execute: async ({ subdirectory }: { subdirectory?: string }) => {
|
||||||
|
try {
|
||||||
|
const dirPath = subdirectory ? path.join(BASE_DIR, subdirectory) : BASE_DIR;
|
||||||
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
const files = entries
|
||||||
|
.filter(entry => entry.isFile())
|
||||||
|
.map(entry => ({
|
||||||
|
name: entry.name,
|
||||||
|
type: path.extname(entry.name) || 'no-extension',
|
||||||
|
relativePath: path.relative(BASE_DIR, path.join(dirPath, entry.name)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const directories = entries
|
||||||
|
.filter(entry => entry.isDirectory())
|
||||||
|
.map(entry => entry.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: dirPath,
|
||||||
|
relativePath: path.relative(BASE_DIR, dirPath) || '.',
|
||||||
|
files,
|
||||||
|
directories,
|
||||||
|
totalFiles: files.length,
|
||||||
|
totalDirectories: directories.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to list files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
analyzeWorkflow: {
|
||||||
|
description: 'Read and analyze a workflow file to understand its structure, agents, and dependencies',
|
||||||
|
inputSchema: z.object({
|
||||||
|
workflowName: z.string().describe('Name of the workflow file to analyze (with or without .json extension)'),
|
||||||
|
}),
|
||||||
|
execute: async ({ workflowName }: { workflowName: string }) => {
|
||||||
|
try {
|
||||||
|
const filename = workflowName.endsWith('.json') ? workflowName : `${workflowName}.json`;
|
||||||
|
const filePath = path.join(BASE_DIR, 'workflows', filename);
|
||||||
|
|
||||||
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const workflow = JSON.parse(content);
|
||||||
|
|
||||||
|
// Extract key information
|
||||||
|
const analysis = {
|
||||||
|
name: workflow.name,
|
||||||
|
description: workflow.description || 'No description',
|
||||||
|
agentCount: workflow.agents ? workflow.agents.length : 0,
|
||||||
|
agents: workflow.agents || [],
|
||||||
|
tools: workflow.tools || {},
|
||||||
|
structure: workflow,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
filePath: path.relative(BASE_DIR, filePath),
|
||||||
|
analysis,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to analyze workflow: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
listMcpServers: {
|
||||||
|
description: 'List all available MCP servers from the configuration',
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
execute: async (): Promise<{ success: boolean, servers: any[], count: number, message: string }> => {
|
||||||
|
try {
|
||||||
|
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
||||||
|
|
||||||
|
// Check if config exists
|
||||||
|
try {
|
||||||
|
await fs.access(configPath);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
servers: [],
|
||||||
|
count: 0,
|
||||||
|
message: 'No MCP servers configured yet',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(configPath, 'utf-8');
|
||||||
|
const config = JSON.parse(content);
|
||||||
|
|
||||||
|
const servers = Object.keys(config.mcpServers || {}).map(name => {
|
||||||
|
const server = config.mcpServers[name];
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type: 'command' in server ? 'stdio' : 'http',
|
||||||
|
command: server.command,
|
||||||
|
url: server.url,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
servers,
|
||||||
|
count: servers.length,
|
||||||
|
message: `Found ${servers.length} MCP server(s)`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
servers: [],
|
||||||
|
count: 0,
|
||||||
|
message: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
listMcpTools: {
|
||||||
|
description: 'List all available tools from a specific MCP server',
|
||||||
|
inputSchema: z.object({
|
||||||
|
serverName: z.string().describe('Name of the MCP server to query'),
|
||||||
|
}),
|
||||||
|
execute: async ({ serverName }: { serverName: string }) => {
|
||||||
|
try {
|
||||||
|
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
||||||
|
const content = await fs.readFile(configPath, 'utf-8');
|
||||||
|
const config = JSON.parse(content);
|
||||||
|
|
||||||
|
const mcpConfig = config.mcpServers[serverName];
|
||||||
|
if (!mcpConfig) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `MCP server '${serverName}' not found in configuration`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transport based on config type
|
||||||
|
let transport;
|
||||||
|
if ('command' in mcpConfig) {
|
||||||
|
transport = new StdioClientTransport({
|
||||||
|
command: mcpConfig.command,
|
||||||
|
args: mcpConfig.args || [],
|
||||||
|
env: mcpConfig.env || {},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
|
||||||
|
} catch {
|
||||||
|
transport = new SSEClientTransport(new URL(mcpConfig.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and connect client
|
||||||
|
const client = new Client({
|
||||||
|
name: 'rowboat-copilot',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect(transport);
|
||||||
|
|
||||||
|
// List available tools
|
||||||
|
const toolsList = await client.listTools();
|
||||||
|
|
||||||
|
// Close connection
|
||||||
|
client.close();
|
||||||
|
transport.close();
|
||||||
|
|
||||||
|
const tools = toolsList.tools.map((t: any) => ({
|
||||||
|
name: t.name,
|
||||||
|
description: t.description || 'No description',
|
||||||
|
inputSchema: t.inputSchema,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
serverName,
|
||||||
|
tools,
|
||||||
|
count: tools.length,
|
||||||
|
message: `Found ${tools.length} tool(s) in MCP server '${serverName}'`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
executeCommand: {
|
||||||
|
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
command: z.string().describe('The shell command to execute (e.g., "ls -la", "cat file.txt")'),
|
||||||
|
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to .rowboat directory)'),
|
||||||
|
}),
|
||||||
|
execute: async ({ command, cwd }: { command: string, cwd?: string }) => {
|
||||||
|
try {
|
||||||
|
const workingDir = cwd ? path.join(BASE_DIR, cwd) : BASE_DIR;
|
||||||
|
const result = await executeCommand(command, { cwd: workingDir });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.exitCode === 0,
|
||||||
|
stdout: result.stdout,
|
||||||
|
stderr: result.stderr,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
command,
|
||||||
|
workingDir,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to execute command: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
command,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import { tool, Tool } from "ai";
|
import { ToolAttachment } from "../entities/agent.js";
|
||||||
import { AgentTool } from "../entities/agent.js";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { McpServers } from "../config/config.js";
|
import { McpServers } from "../config/config.js";
|
||||||
import { getMcpClient } from "./mcp.js";
|
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client";
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
||||||
import { executeCommand } from "./command-executor.js";
|
|
||||||
import { loadWorkflow } from "./utils.js";
|
|
||||||
import { AssistantMessage } from "../entities/message.js";
|
import { AssistantMessage } from "../entities/message.js";
|
||||||
import { executeWorkflow } from "./exec-workflow.js";
|
import { BuiltinTools } from "./builtin-tools.js";
|
||||||
import readline from "readline";
|
import { streamAgent } from "./agent.js";
|
||||||
|
|
||||||
async function execMcpTool(agentTool: z.infer<typeof AgentTool> & { type: "mcp" }, input: any): Promise<any> {
|
async function execMcpTool(agentTool: z.infer<typeof ToolAttachment> & { type: "mcp" }, input: any): Promise<any> {
|
||||||
// load mcp configuration from the tool
|
// load mcp configuration from the tool
|
||||||
const mcpConfig = McpServers[agentTool.mcpServerName];
|
const mcpConfig = McpServers[agentTool.mcpServerName];
|
||||||
if (!mcpConfig) {
|
if (!mcpConfig) {
|
||||||
|
|
@ -57,34 +53,12 @@ async function execMcpTool(agentTool: z.infer<typeof AgentTool> & { type: "mcp"
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execBashTool(agentTool: z.infer<typeof AgentTool>, input: any): Promise<any> {
|
async function execAgentTool(agentTool: z.infer<typeof ToolAttachment> & { type: "agent" }, input: any): Promise<any> {
|
||||||
const result = await executeCommand(input.command as string);
|
|
||||||
return {
|
|
||||||
stdout: result.stdout,
|
|
||||||
stderr: result.stderr,
|
|
||||||
exitCode: result.exitCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function execAskHumanTool(agentTool: z.infer<typeof AgentTool>, question: string): Promise<string> {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
let p = new Promise<string>((resolve, reject) => {
|
|
||||||
rl.question(`>> Provide answer to: ${question}:\n\n`, (answer) => {
|
|
||||||
resolve(answer);
|
|
||||||
rl.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const answer = await p;
|
|
||||||
return answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function execWorkflowTool(agentTool: z.infer<typeof AgentTool> & { type: "workflow" }, input: any): Promise<any> {
|
|
||||||
let lastMsg: z.infer<typeof AssistantMessage> | null = null;
|
let lastMsg: z.infer<typeof AssistantMessage> | null = null;
|
||||||
for await (const event of executeWorkflow(agentTool.name, input.message)) {
|
for await (const event of streamAgent({
|
||||||
|
agent: agentTool.name,
|
||||||
|
input: JSON.stringify(input),
|
||||||
|
})) {
|
||||||
if (event.type === "message" && event.message.role === "assistant") {
|
if (event.type === "message" && event.message.role === "assistant") {
|
||||||
lastMsg = event.message;
|
lastMsg = event.message;
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +68,7 @@ async function execWorkflowTool(agentTool: z.infer<typeof AgentTool> & { type: "
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastMsg) {
|
if (!lastMsg) {
|
||||||
throw new Error("No message received from workflow");
|
throw new Error("No message received from agent");
|
||||||
}
|
}
|
||||||
if (typeof lastMsg.content === "string") {
|
if (typeof lastMsg.content === "string") {
|
||||||
return lastMsg.content;
|
return lastMsg.content;
|
||||||
|
|
@ -107,18 +81,17 @@ async function execWorkflowTool(agentTool: z.infer<typeof AgentTool> & { type: "
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function execTool(agentTool: z.infer<typeof AgentTool>, input: any): Promise<any> {
|
export async function execTool(agentTool: z.infer<typeof ToolAttachment>, input: any): Promise<any> {
|
||||||
switch (agentTool.type) {
|
switch (agentTool.type) {
|
||||||
case "mcp":
|
case "mcp":
|
||||||
return execMcpTool(agentTool, input);
|
return execMcpTool(agentTool, input);
|
||||||
case "workflow":
|
case "agent":
|
||||||
return execWorkflowTool(agentTool, input);
|
return execAgentTool(agentTool, input);
|
||||||
case "builtin":
|
case "builtin":
|
||||||
switch (agentTool.name) {
|
const builtinTool = BuiltinTools[agentTool.name];
|
||||||
case "bash":
|
if (!builtinTool || !builtinTool.execute) {
|
||||||
return execBashTool(agentTool, input);
|
throw new Error(`Unsupported builtin tool: ${agentTool.name}`);
|
||||||
default:
|
|
||||||
throw new Error(`Unknown builtin tool: ${agentTool.name}`);
|
|
||||||
}
|
}
|
||||||
|
return builtinTool.execute(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,449 +0,0 @@
|
||||||
import { loadWorkflow } from "./utils.js";
|
|
||||||
import { MessageList, AssistantMessage, AssistantContentPart, Message, ToolMessage, ToolCallPart } from "../entities/message.js";
|
|
||||||
import { LlmStepStreamEvent } from "../entities/llm-step-event.js";
|
|
||||||
import { AgentNode } from "./agent.js";
|
|
||||||
import { z } from "zod";
|
|
||||||
import path from "path";
|
|
||||||
import { WorkDir } from "../config/config.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import { createInterface, Interface } from "node:readline/promises";
|
|
||||||
import { FunctionsRegistry } from "../registry/functions.js";
|
|
||||||
import { RunEvent } from "../entities/workflow-event.js";
|
|
||||||
import { execAskHumanTool, execTool } from "./exec-tool.js";
|
|
||||||
import { AgentTool } from "../entities/agent.js";
|
|
||||||
import { runIdGenerator } from "./run-id-gen.js";
|
|
||||||
import { Workflow } from "../entities/workflow.js";
|
|
||||||
|
|
||||||
const MappedToolCall = z.object({
|
|
||||||
toolCall: ToolCallPart,
|
|
||||||
agentTool: AgentTool,
|
|
||||||
});
|
|
||||||
|
|
||||||
const State = z.object({
|
|
||||||
stepIndex: z.number(),
|
|
||||||
messages: MessageList,
|
|
||||||
workflow: Workflow.nullable(),
|
|
||||||
pendingToolCallId: z.string().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
class StateBuilder {
|
|
||||||
private state: z.infer<typeof State> = {
|
|
||||||
stepIndex: 0,
|
|
||||||
messages: [],
|
|
||||||
workflow: null,
|
|
||||||
pendingToolCallId: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
ingest(event: z.infer<typeof RunEvent>) {
|
|
||||||
switch (event.type) {
|
|
||||||
case "start":
|
|
||||||
this.state.workflow = event.workflow;
|
|
||||||
break;
|
|
||||||
case "step-start":
|
|
||||||
this.state.stepIndex = event.stepIndex;
|
|
||||||
break;
|
|
||||||
case "message":
|
|
||||||
this.state.messages.push(event.message);
|
|
||||||
this.state.pendingToolCallId = null;
|
|
||||||
break;
|
|
||||||
case "pause-for-human-input":
|
|
||||||
this.state.pendingToolCallId = event.toolCallId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(): z.infer<typeof State> {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RunLogger {
|
|
||||||
private logFile: string;
|
|
||||||
private fileHandle: fs.WriteStream;
|
|
||||||
|
|
||||||
ensureRunsDir(workflowId: string) {
|
|
||||||
const runsDir = path.join(WorkDir, "runs", workflowId);
|
|
||||||
if (!fs.existsSync(runsDir)) {
|
|
||||||
fs.mkdirSync(runsDir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(workflowId: string, runId: string) {
|
|
||||||
this.ensureRunsDir(workflowId);
|
|
||||||
this.logFile = path.join(WorkDir, "runs", `${runId}.jsonl`);
|
|
||||||
this.fileHandle = fs.createWriteStream(this.logFile, {
|
|
||||||
flags: "a",
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
log(event: z.infer<typeof RunEvent>) {
|
|
||||||
this.fileHandle.write(JSON.stringify(event) + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.fileHandle.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogAndYield {
|
|
||||||
private logger: RunLogger
|
|
||||||
|
|
||||||
constructor(logger: RunLogger) {
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
async *logAndYield(event: z.infer<typeof RunEvent>): AsyncGenerator<z.infer<typeof RunEvent>, void, unknown> {
|
|
||||||
const ev = {
|
|
||||||
...event,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
this.logger.log(ev);
|
|
||||||
yield ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StreamStepMessageBuilder {
|
|
||||||
private parts: z.infer<typeof AssistantContentPart>[] = [];
|
|
||||||
private textBuffer: string = "";
|
|
||||||
private reasoningBuffer: string = "";
|
|
||||||
|
|
||||||
flushBuffers() {
|
|
||||||
if (this.reasoningBuffer) {
|
|
||||||
this.parts.push({ type: "reasoning", text: this.reasoningBuffer });
|
|
||||||
this.reasoningBuffer = "";
|
|
||||||
}
|
|
||||||
if (this.textBuffer) {
|
|
||||||
this.parts.push({ type: "text", text: this.textBuffer });
|
|
||||||
this.textBuffer = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ingest(event: z.infer<typeof LlmStepStreamEvent>) {
|
|
||||||
switch (event.type) {
|
|
||||||
case "reasoning-start":
|
|
||||||
case "reasoning-end":
|
|
||||||
case "text-start":
|
|
||||||
case "text-end":
|
|
||||||
this.flushBuffers();
|
|
||||||
break;
|
|
||||||
case "reasoning-delta":
|
|
||||||
this.reasoningBuffer += event.delta;
|
|
||||||
break;
|
|
||||||
case "text-delta":
|
|
||||||
this.textBuffer += event.delta;
|
|
||||||
break;
|
|
||||||
case "tool-call":
|
|
||||||
this.parts.push({
|
|
||||||
type: "tool-call",
|
|
||||||
toolCallId: event.toolCallId,
|
|
||||||
toolName: event.toolName,
|
|
||||||
arguments: event.input,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(): z.infer<typeof AssistantMessage> {
|
|
||||||
this.flushBuffers();
|
|
||||||
return {
|
|
||||||
role: "assistant",
|
|
||||||
content: this.parts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFunction(id: string) {
|
|
||||||
const func = FunctionsRegistry[id];
|
|
||||||
if (!func) {
|
|
||||||
throw new Error(`Function ${id} not found`);
|
|
||||||
}
|
|
||||||
return func;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* executeWorkflow(id: string, input: string, interactive: boolean = true, asTool: boolean = false): AsyncGenerator<z.infer<typeof RunEvent>, void, unknown> {
|
|
||||||
const runId = runIdGenerator.next();
|
|
||||||
yield* runFromState({
|
|
||||||
id,
|
|
||||||
runId,
|
|
||||||
state: {
|
|
||||||
stepIndex: 0,
|
|
||||||
messages: [{
|
|
||||||
role: "user",
|
|
||||||
content: input,
|
|
||||||
}],
|
|
||||||
workflow: null,
|
|
||||||
pendingToolCallId: null,
|
|
||||||
},
|
|
||||||
interactive,
|
|
||||||
asTool,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* resumeWorkflow(runId: string, input: string, interactive: boolean = false): AsyncGenerator<z.infer<typeof RunEvent>, void, unknown> {
|
|
||||||
// read a run.jsonl file line by line and build state
|
|
||||||
const builder = new StateBuilder();
|
|
||||||
let rl: Interface | null = null;
|
|
||||||
let stream: fs.ReadStream | null = null;
|
|
||||||
try {
|
|
||||||
const logFile = path.join(WorkDir, "runs", `${runId}.jsonl`);
|
|
||||||
stream = fs.createReadStream(logFile, { encoding: "utf8" });
|
|
||||||
rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
||||||
for await (const line of rl) {
|
|
||||||
if (line.trim() === "") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// console.error('processing line', line);
|
|
||||||
const parsed = JSON.parse(line);
|
|
||||||
// console.error('parsed');
|
|
||||||
const event = RunEvent.parse(parsed);
|
|
||||||
// console.error('zod parsed');
|
|
||||||
builder.ingest(event);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// console.error("Failed to resume workflow:", error);
|
|
||||||
// yield {
|
|
||||||
// type: "error",
|
|
||||||
// error: error instanceof Error ? error.message : String(error),
|
|
||||||
// };
|
|
||||||
} finally {
|
|
||||||
rl?.close();
|
|
||||||
stream?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { workflow, messages, stepIndex, pendingToolCallId } = builder.get();
|
|
||||||
if (!workflow) {
|
|
||||||
throw new Error(`Workflow not found for run ${runId}`);
|
|
||||||
}
|
|
||||||
if (!pendingToolCallId) {
|
|
||||||
throw new Error(`No pending tool call found for run ${runId}`);
|
|
||||||
}
|
|
||||||
const stepId = workflow.steps[stepIndex].id;
|
|
||||||
|
|
||||||
// append user input as message
|
|
||||||
const logger = new RunLogger(workflow.name, runId);
|
|
||||||
const ly = new LogAndYield(logger);
|
|
||||||
yield *ly.logAndYield({
|
|
||||||
type: "resume"
|
|
||||||
});
|
|
||||||
|
|
||||||
// append user input as message
|
|
||||||
const resultMsg: z.infer<typeof ToolMessage> = {
|
|
||||||
role: "tool",
|
|
||||||
content: JSON.stringify(input),
|
|
||||||
toolCallId: pendingToolCallId,
|
|
||||||
toolName: "ask-human",
|
|
||||||
};
|
|
||||||
messages.push(resultMsg);
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "tool-result",
|
|
||||||
stepId,
|
|
||||||
toolName: "ask-human",
|
|
||||||
result: input,
|
|
||||||
});
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "message",
|
|
||||||
stepId,
|
|
||||||
message: resultMsg,
|
|
||||||
});
|
|
||||||
|
|
||||||
yield* runFromState({
|
|
||||||
id: workflow.name,
|
|
||||||
runId,
|
|
||||||
state: {
|
|
||||||
stepIndex,
|
|
||||||
messages,
|
|
||||||
workflow,
|
|
||||||
pendingToolCallId,
|
|
||||||
},
|
|
||||||
interactive,
|
|
||||||
asTool: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function* runFromState(opts: {
|
|
||||||
id: string;
|
|
||||||
runId: string;
|
|
||||||
state: z.infer<typeof State>;
|
|
||||||
interactive: boolean;
|
|
||||||
asTool: boolean;
|
|
||||||
}) {
|
|
||||||
const { id, runId, state, interactive, asTool } = opts;
|
|
||||||
let stepIndex = state.stepIndex;
|
|
||||||
let messages = [...state.messages];
|
|
||||||
let workflow = state.workflow;
|
|
||||||
|
|
||||||
const logger = new RunLogger(id, runId);
|
|
||||||
const ly = new LogAndYield(logger);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!workflow) {
|
|
||||||
workflow = loadWorkflow(id);
|
|
||||||
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "start",
|
|
||||||
runId,
|
|
||||||
workflowId: id,
|
|
||||||
workflow,
|
|
||||||
interactive,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const step = workflow.steps[stepIndex];
|
|
||||||
const node = step.type === "agent" ? new AgentNode(step.id, asTool) : loadFunction(step.id);
|
|
||||||
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "step-start",
|
|
||||||
stepIndex,
|
|
||||||
stepId: step.id,
|
|
||||||
stepType: step.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageBuilder = new StreamStepMessageBuilder();
|
|
||||||
|
|
||||||
// stream response from agent
|
|
||||||
for await (const event of node.execute(messages)) {
|
|
||||||
// console.log(" - event", JSON.stringify(event));
|
|
||||||
messageBuilder.ingest(event);
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "stream-event",
|
|
||||||
stepId: step.id,
|
|
||||||
event: event,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// build and emit final message from agent response
|
|
||||||
const msg = messageBuilder.get();
|
|
||||||
messages.push(msg);
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "message",
|
|
||||||
stepId: step.id,
|
|
||||||
message: msg,
|
|
||||||
});
|
|
||||||
|
|
||||||
// handle tool calls
|
|
||||||
const tools = node.tools();
|
|
||||||
const mappedToolCalls: z.infer<typeof MappedToolCall>[] = [];
|
|
||||||
let msgToolCallParts: z.infer<typeof ToolCallPart>[] = [];
|
|
||||||
if (msg.content instanceof Array) {
|
|
||||||
msgToolCallParts = msg.content.filter(part => part.type === "tool-call");
|
|
||||||
}
|
|
||||||
const hasToolCalls = msgToolCallParts.length > 0;
|
|
||||||
|
|
||||||
// validate and map tool calls
|
|
||||||
for (const part of msgToolCallParts) {
|
|
||||||
const agentTool = tools[part.toolName];
|
|
||||||
if (!agentTool) {
|
|
||||||
throw new Error(`Tool ${part.toolName} not found`);
|
|
||||||
}
|
|
||||||
mappedToolCalls.push({
|
|
||||||
toolCall: part,
|
|
||||||
agentTool: agentTool,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// first, exec all tool calls other than ask-human
|
|
||||||
for (const call of mappedToolCalls) {
|
|
||||||
const { agentTool, toolCall } = call;
|
|
||||||
if (agentTool.type === "builtin" && agentTool.name === "ask-human") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "tool-invocation",
|
|
||||||
stepId: step.id,
|
|
||||||
toolName: toolCall.toolName,
|
|
||||||
input: JSON.stringify(toolCall.arguments),
|
|
||||||
});
|
|
||||||
const result = await execTool(agentTool, toolCall.arguments);
|
|
||||||
const resultMsg: z.infer<typeof ToolMessage> = {
|
|
||||||
role: "tool",
|
|
||||||
content: JSON.stringify(result),
|
|
||||||
toolCallId: toolCall.toolCallId,
|
|
||||||
toolName: toolCall.toolName,
|
|
||||||
};
|
|
||||||
messages.push(resultMsg);
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "tool-result",
|
|
||||||
stepId: step.id,
|
|
||||||
toolName: toolCall.toolName,
|
|
||||||
result: result,
|
|
||||||
});
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "message",
|
|
||||||
stepId: step.id,
|
|
||||||
message: resultMsg,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle ask-tool call execution
|
|
||||||
for (const call of mappedToolCalls) {
|
|
||||||
const { agentTool, toolCall } = call;
|
|
||||||
if (agentTool.type !== "builtin" || agentTool.name !== "ask-human") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "tool-invocation",
|
|
||||||
stepId: step.id,
|
|
||||||
toolName: toolCall.toolName,
|
|
||||||
input: JSON.stringify(toolCall.arguments),
|
|
||||||
});
|
|
||||||
|
|
||||||
// if running in background mode, exit here
|
|
||||||
if (!interactive) {
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "pause-for-human-input",
|
|
||||||
toolCallId: toolCall.toolCallId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await execAskHumanTool(agentTool, toolCall.arguments.question as string);
|
|
||||||
const resultMsg: z.infer<typeof ToolMessage> = {
|
|
||||||
role: "tool",
|
|
||||||
content: JSON.stringify(result),
|
|
||||||
toolCallId: toolCall.toolCallId,
|
|
||||||
toolName: toolCall.toolName,
|
|
||||||
};
|
|
||||||
messages.push(resultMsg);
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "tool-result",
|
|
||||||
stepId: step.id,
|
|
||||||
toolName: toolCall.toolName,
|
|
||||||
result: result,
|
|
||||||
});
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "message",
|
|
||||||
stepId: step.id,
|
|
||||||
message: resultMsg,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "step-end",
|
|
||||||
stepIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if the agent response had tool calls, replay this agent
|
|
||||||
if (hasToolCalls) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, move to the next step
|
|
||||||
stepIndex++;
|
|
||||||
if (stepIndex >= workflow.steps.length) {
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "end",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// console.log('\n\n', JSON.stringify(messages, null, 2));
|
|
||||||
} catch (error) {
|
|
||||||
yield* ly.logAndYield({
|
|
||||||
type: "error",
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
logger.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { MessageList } from "../entities/message.js";
|
import { MessageList } from "../entities/message.js";
|
||||||
import { LlmStepStreamEvent } from "../entities/llm-step-event.js";
|
import { LlmStepStreamEvent } from "../entities/llm-step-events.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AgentTool } from "../entities/agent.js";
|
import { ToolAttachment } from "../entities/agent.js";
|
||||||
|
|
||||||
export type StepInputT = z.infer<typeof MessageList>;
|
export type StepInputT = z.infer<typeof MessageList>;
|
||||||
export type StepOutputT = AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, void, unknown>;
|
export type StepOutputT = AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, void, unknown>;
|
||||||
|
|
@ -9,5 +9,5 @@ export type StepOutputT = AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, voi
|
||||||
export interface Step {
|
export interface Step {
|
||||||
execute(input: StepInputT): StepOutputT;
|
execute(input: StepInputT): StepOutputT;
|
||||||
|
|
||||||
tools(): Record<string, z.infer<typeof AgentTool>>;
|
tools(): Record<string, z.infer<typeof ToolAttachment>>;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RunEvent } from "../entities/workflow-event.js";
|
import { RunEvent } from "../entities/run-events.js";
|
||||||
import { LlmStepStreamEvent } from "../entities/llm-step-event.js";
|
import { LlmStepStreamEvent } from "../entities/llm-step-events.js";
|
||||||
|
|
||||||
export interface StreamRendererOptions {
|
export interface StreamRendererOptions {
|
||||||
showHeaders?: boolean;
|
showHeaders?: boolean;
|
||||||
|
|
@ -27,11 +27,11 @@ export class StreamRenderer {
|
||||||
render(event: z.infer<typeof RunEvent>) {
|
render(event: z.infer<typeof RunEvent>) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "start": {
|
case "start": {
|
||||||
this.onWorkflowStart(event.workflowId, event.runId, event.interactive);
|
this.onStart(event.agentId, event.runId, event.interactive);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "step-start": {
|
case "step-start": {
|
||||||
this.onStepStart(event.stepIndex, event.stepId, event.stepType);
|
this.onStepStart();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "stream-event": {
|
case "stream-event": {
|
||||||
|
|
@ -43,23 +43,23 @@ export class StreamRenderer {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "tool-invocation": {
|
case "tool-invocation": {
|
||||||
this.onStepToolInvocation(event.stepId, event.toolName, event.input);
|
this.onStepToolInvocation(event.toolName, event.input);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "tool-result": {
|
case "tool-result": {
|
||||||
this.onStepToolResult(event.stepId, event.toolName, event.result);
|
this.onStepToolResult(event.toolName, event.result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "step-end": {
|
case "step-end": {
|
||||||
this.onStepEnd(event.stepIndex);
|
this.onStepEnd();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "end": {
|
case "end": {
|
||||||
this.onWorkflowEnd();
|
this.onEnd();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "error": {
|
case "error": {
|
||||||
this.onWorkflowError(event.error);
|
this.onError(event.error);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,29 +94,29 @@ export class StreamRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWorkflowStart(workflowId: string, runId: string, interactive: boolean) {
|
private onStart(workflowId: string, runId: string, interactive: boolean) {
|
||||||
this.write("\n");
|
this.write("\n");
|
||||||
this.write(this.bold(`▶ Workflow ${workflowId} (run ${runId})`));
|
this.write(this.bold(`▶ Workflow ${workflowId} (run ${runId})`));
|
||||||
if (!interactive) this.write(this.dim(" (--no-interactive)"));
|
if (!interactive) this.write(this.dim(" (--no-interactive)"));
|
||||||
this.write("\n");
|
this.write("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWorkflowEnd() {
|
private onEnd() {
|
||||||
this.write(this.bold("\n■ Workflow complete\n"));
|
this.write(this.bold("\n■ Workflow complete\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWorkflowError(error: string) {
|
private onError(error: string) {
|
||||||
this.write(this.red(`\n✖ Workflow error: ${error}\n`));
|
this.write(this.red(`\n✖ Workflow error: ${error}\n`));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStepStart(stepIndex: number, stepId: string, stepType: "agent" | "function") {
|
private onStepStart() {
|
||||||
this.write("\n");
|
this.write("\n");
|
||||||
this.write(this.cyan(`─ Step ${stepIndex} [${stepType}]`));
|
this.write(this.cyan(`─ Step started`));
|
||||||
this.write("\n");
|
this.write("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStepEnd(stepIndex: number) {
|
private onStepEnd() {
|
||||||
this.write(this.dim(`✓ Step ${stepIndex} finished\n`));
|
this.write(this.dim(`✓ Step finished\n`));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStepMessage(stepIndex: number, message: any) {
|
private onStepMessage(stepIndex: number, message: any) {
|
||||||
|
|
@ -131,7 +131,7 @@ export class StreamRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStepToolInvocation(stepId: string, toolName: string, input: string) {
|
private onStepToolInvocation(toolName: string, input: string) {
|
||||||
this.write(this.cyan(`\n→ Tool invoke ${toolName}`));
|
this.write(this.cyan(`\n→ Tool invoke ${toolName}`));
|
||||||
if (input && input.length) {
|
if (input && input.length) {
|
||||||
this.write("\n" + this.dim(this.indent(this.truncate(input))) + "\n");
|
this.write("\n" + this.dim(this.indent(this.truncate(input))) + "\n");
|
||||||
|
|
@ -140,7 +140,7 @@ export class StreamRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStepToolResult(stepId: string, toolName: string, result: unknown) {
|
private onStepToolResult(toolName: string, result: unknown) {
|
||||||
const res = this.truncate(JSON.stringify(result, null, this.options.jsonIndent));
|
const res = this.truncate(JSON.stringify(result, null, this.options.jsonIndent));
|
||||||
this.write(this.cyan(`\n← Tool result ${toolName}\n`));
|
this.write(this.cyan(`\n← Tool result ${toolName}\n`));
|
||||||
this.write(this.dim(this.indent(res)) + "\n");
|
this.write(this.dim(this.indent(res)) + "\n");
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { WorkDir } from "../config/config.js";
|
|
||||||
import { Workflow } from "../entities/workflow.js";
|
|
||||||
|
|
||||||
export function loadWorkflow(id: string) {
|
|
||||||
const workflowPath = path.join(WorkDir, "workflows", `${id}.json`);
|
|
||||||
const workflow = fs.readFileSync(workflowPath, "utf8");
|
|
||||||
return Workflow.parse(JSON.parse(workflow));
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { GetDate } from "../functions/get_date.js";
|
|
||||||
import { Step } from "../lib/step.js";
|
|
||||||
|
|
||||||
export const FunctionsRegistry: Record<string, Step> = {
|
|
||||||
get_date: new GetDate(),
|
|
||||||
} as const;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { startCopilot } from "./application/assistant/chat.js";
|
|
||||||
|
|
||||||
export const start = () => {
|
|
||||||
startCopilot().catch((err) => {
|
|
||||||
console.error("Failed to run copilot:", err);
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue