diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 880a8a617..731555a01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,29 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + # `undici` (a direct dependency, ^8.11.2 as of LOU-K3's Cloudflare + # Worker LLM provider support) declares an engines requirement of + # node >=22.19.0. Under Node 20, requiring it (indirectly, via + # anything that touches its Cache/fetch internals) throws + # `TypeError: webidl.util.markAsUncloneable is not a function` + # instead of just an EBADENGINE warning - which was crashing + # whichever vitest worker hit it first and cascading into "0 test" + # failures across the rest of that worker's queue. Node 20 was + # already below what a fresh dependency needs; bump to match. + node-version: '22' cache: 'npm' - run: npm ci - run: npx tsc --noEmit - run: npm run lint - - run: npm run test:coverage + # Build the root SDK's dist *before* running tests: + # packages/create-loushy-agent/src/template.e2e.test.ts `npm pack`s the + # root SDK's own source tree to install it into a generated project, + # which needs dist/ (package.json's main/module/types) to already + # exist - it was previously built only in the step after tests, so + # this test always failed once it actually got a chance to run. - run: npm run build + # packages/create-loushy-agent's own tests execFileSync its bin/cli.js, + # which requires ITS dist/index.js to exist too - build it before the + # test step (this is a separate package from the root SDK above). + - run: npm run build --workspace=packages/create-loushy-agent + - run: npm run test:coverage diff --git a/apps/agent-forge/eslint.config.mjs b/apps/agent-forge/eslint.config.mjs new file mode 100644 index 000000000..c9f01952c --- /dev/null +++ b/apps/agent-forge/eslint.config.mjs @@ -0,0 +1,21 @@ +// Mirrors the root repo's eslint.config.mjs conventions (typescript-eslint +// recommended, no-explicit-any / no-unused-vars / ban-ts-comment downgraded +// to 'warn' rather than turned off) so this app's lint output stays +// consistent with `npm run lint` at the repo root, rather than inventing a +// divergent lint setup for this one workspace. +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['dist/**', 'node_modules/**', 'coverage/**'], + }, + ...tseslint.configs.recommended, + { + files: ['src/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/ban-ts-comment': 'warn', + }, + } +); diff --git a/apps/agent-forge/index.html b/apps/agent-forge/index.html new file mode 100644 index 000000000..f2f825d96 --- /dev/null +++ b/apps/agent-forge/index.html @@ -0,0 +1,20 @@ + + + + + + Agent Forge + + + + +
+ + + diff --git a/apps/agent-forge/package.json b/apps/agent-forge/package.json new file mode 100644 index 000000000..0e3e2f939 --- /dev/null +++ b/apps/agent-forge/package.json @@ -0,0 +1,33 @@ +{ + "name": "@loushy/agent-forge", + "version": "0.0.1", + "private": true, + "description": "Agent Forge - visual dashboard for creating, running and debugging agents built with @loushy/build-ai-agent (LOU-L: app shell & data model).", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit -p tsconfig.json && vite build", + "preview": "vite preview", + "test": "vitest", + "lint": "eslint src", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@loushy/build-ai-agent": "file:../..", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "yaml": "^2.9.1" + }, + "devDependencies": { + "@ai-sdk/anthropic": "^0.0.42", + "@ai-sdk/openai": "^0.0.42", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^25.0.1", + "ollama-ai-provider": "^1.2.0", + "typescript": "^5.9.3", + "vite": "^5.4.11", + "vitest": "^1.0.0" + } +} diff --git a/apps/agent-forge/src/App.tsx b/apps/agent-forge/src/App.tsx new file mode 100644 index 000000000..7f3e01ef8 --- /dev/null +++ b/apps/agent-forge/src/App.tsx @@ -0,0 +1,23 @@ +import './components/layout.css'; +import { AppStateProvider } from './state/AppState'; +import { Topbar } from './components/Topbar'; +import { LeftRail } from './components/LeftRail'; +import { CanvasArea } from './components/CanvasArea'; +import { Inspector } from './components/Inspector'; +import { BottomDrawer } from './components/BottomDrawer'; + +export function App() { + return ( + +
+ +
+ + + +
+ +
+
+ ); +} diff --git a/apps/agent-forge/src/components/BottomDrawer.tsx b/apps/agent-forge/src/components/BottomDrawer.tsx new file mode 100644 index 000000000..289500146 --- /dev/null +++ b/apps/agent-forge/src/components/BottomDrawer.tsx @@ -0,0 +1,44 @@ +import { useAppState } from '../state/AppState'; +import type { DrawerTab } from '../state/AppState'; + +const TABS: { id: DrawerTab; label: string }[] = [ + { id: 'chat', label: 'Chat' }, + { id: 'logs', label: 'Logs' }, + { id: 'trace', label: 'Trace' }, + { id: 'output', label: 'Output' }, + { id: 'settings', label: 'Settings' }, +]; + +/** + * Tab-switching chrome only - each tab's real content is a later epic: + * Chat -> LOU-P, Logs/Trace -> LOU-O, Settings -> LOU-R. "Output" shows the + * current in-memory `AgentSpec` as JSON, which is real (LOU-L2 data), not a + * placeholder. + */ +export function BottomDrawer() { + const { drawerTab, setDrawerTab, spec } = useAppState(); + + return ( +
+
+ {TABS.map((tab) => ( + + ))} +
+
+
+ {drawerTab === 'chat' &&
Chat with a running agent is wired up in LOU-P.
} + {drawerTab === 'logs' &&
Live execution logs are wired up in LOU-O (debug console).
} + {drawerTab === 'trace' &&
Execution trace spans are wired up in LOU-O (debug console).
} + {drawerTab === 'output' &&
{JSON.stringify(spec, null, 2)}
} + {drawerTab === 'settings' &&
Provider keys and deploy target settings are wired up in LOU-R.
} +
+
+ ); +} diff --git a/apps/agent-forge/src/components/CanvasArea.tsx b/apps/agent-forge/src/components/CanvasArea.tsx new file mode 100644 index 000000000..30a22999f --- /dev/null +++ b/apps/agent-forge/src/components/CanvasArea.tsx @@ -0,0 +1,17 @@ +/** + * Canvas placeholder (LOU-L1). Real ReactFlow-based node-graph editing + * lands in LOU-M - this just occupies the layout's center column with a + * styled placeholder so the app shell is complete without pretending to be + * an interactive canvas. + */ +export function CanvasArea() { + return ( +
+
+
Canvas
+ The node-graph editor (ReactFlow) is built in LOU-M. This placeholder confirms the app shell's layout, + theming and data model without a real canvas yet. +
+
+ ); +} diff --git a/apps/agent-forge/src/components/Inspector.tsx b/apps/agent-forge/src/components/Inspector.tsx new file mode 100644 index 000000000..6900f81e9 --- /dev/null +++ b/apps/agent-forge/src/components/Inspector.tsx @@ -0,0 +1,101 @@ +import { useAppState } from '../state/AppState'; + +const KNOWN_TOOLS = ['http', 'current-date', 'day-name']; +const KNOWN_PROVIDERS = ['mock', 'openai', 'anthropic', 'ollama', 'openrouter']; + +/** + * Inspector fields here are limited to what `AgentSpec` actually has a + * field for (name/prompt/provider/tools/policy.requiresApproval) - the + * mockup also shows per-node fields like temperature, max-steps and hook + * wiring that don't exist in `AgentSpec` yet (those are richer per-node + * config LOU-M's real graph editor will need to add to the spec/schema + * first). This deliberately edits the single underlying `AgentSpec` + * directly rather than a selected canvas node, since there is no real + * canvas/node-selection yet (LOU-M). + */ +export function Inspector() { + const { spec, setSpec } = useAppState(); + + function toggleTool(tool: string) { + setSpec((prev) => { + const tools = prev.tools ?? []; + const has = tools.includes(tool); + return { ...prev, tools: has ? tools.filter((t) => t !== tool) : [...tools, tool] }; + }); + } + + return ( +
+
+
Selected agent
+
{spec.name}
+
+
+
+ + setSpec((prev) => ({ ...prev, name: e.target.value }))} + /> +
+
+ +
+ + + setSpec((prev) => ({ ...prev, provider: { ...prev.provider, model: e.target.value } })) + } + /> +
+
+
+ +