⭐ Found Forgewisp useful? Star the repo on GitHub.
Safe, function-calling AI agents for the browser. Register your existing frontend functions as tools, get schema validation, a risk-tier execution model (read / write / destructive), streaming, and a reasoning stream — all with no mandatory backend.
packages/core—@forgewisp/core, the library.packages/bundled-tools—@forgewisp/bundled-tools, a catalog of ready-to-register browser-effects tools (time, UUIDs, safe math, hashing, base64, viewport/battery/localStorage reads, clipboard/speech/download/ geolocation/localStorage writes, a destructive localStorage remove, and an agent planning scratchpad persisted inlocalStorage).packages/mcp—@forgewisp/mcp, an opt-in adapter that connects to an MCP server over the Streamable HTTP transport and adapts its tools intoFunctionDefinitions registered through the agent's existing path — so core's validation, risk tiers, confirmation, and audit log apply to MCP tools unchanged. Supports OAuth 2.1 + PKCE. Adds@modelcontextprotocol/sdkas its only runtime dep (kept out of core for users who don't need MCP).apps/demo— vanilla TypeScript + Vite demo: a task-manager UI driven by an AI agent whose tools mutate local state.apps/bundled-demo— vanilla TypeScript + Vite showcase that registers all of@forgewisp/bundled-toolsand renders a toolkit sidebar plus an artifacts panel fed by the audit log.apps/planning-demo— vanilla TypeScript + Vite demo of the agent planning tools (a job-tracking scratchpad persisted inlocalStorage).apps/subagent-demo— vanilla TypeScript + Vite orchestration demo: a pure-orchestrator parent agent that delegates read-only sub-tasks to fresh subagents viaspawnSubagent, with a live "Subagent Runs" board fed by the audit log.apps/mcp-demo— vanilla TypeScript + Vite showcase of@forgewisp/mcp: an MCP-only agent that connects to one or more Streamable-HTTP MCP servers at runtime and drives their tools, with a tier- grouped connected-tools sidebar.
- Register frontend functions as agent tools with a JSON Schema for args.
- Bundled tool catalog —
@forgewisp/bundled-toolsships ready-to-register browser-effects tools with strict JSON Schemas and safe handlers, so you can give an agent real browser capabilities in oneforEach. Seepackages/bundled-tools. - Risk tiers:
readruns immediately;writeanddestructivepause for developer-supplied confirmation. Confirmation UI is always rendered from validated args — never from LLM-generated text. - Streaming for OpenAI-compatible endpoints (OpenAI, LiteLLM, OpenRouter, vLLM, …).
- Reasoning stream (four modes, set via
streaming.reasoning):none— no reasoning separation;onReasoningChunkis never called.extended— OpenAI o1/o3reasoning_tokenscount (surfaced as an annotation).tag-based— models that wrap reasoning in<thinking>…</thinking>(or any developer-specified tag). Forgewisp parses the stream in real time and routes inner content toonReasoningChunk, outer content toonTextChunk. Handles tags split across chunk boundaries.native— the server streams reasoning in a separate delta field (reasoningfor Ollama,reasoning_contentfor vLLM/DashScope); each reasoning delta is routed straight toonReasoningChunk. (The demo uses this mode.)
- Audit log of every function request, validation result, confirmation outcome, and execution result.
- Subagent orchestration —
createSubagentToolbuilds aspawnSubagenttool that delegates a self-contained sub-task to a fresh child agent running its own tool loop. Only a trimmed result returns to the parent, so the child's intermediate reasoning and tool calls stay out of the parent's context. The child reuses the parent's connection, confirmation, and audit config — but not its system prompt or streaming UI callbacks. Seepackages/core.
pnpm install
pnpm build
pnpm testRun a demo:
pnpm dev
# open http://localhost:5173There are five apps — apps/demo (task manager), apps/bundled-demo
(bundled-tools showcase), apps/planning-demo (planning tools),
apps/subagent-demo (subagent orchestration), and apps/mcp-demo (MCP adapter
showcase) — and all Vite dev servers default to port 5173, so pnpm dev will
start one there and bump the others to the next free ports. To run just one,
work in its directory:
cd apps/bundled-demo && pnpm dev # the showcase
# or
cd apps/demo && pnpm dev # the task manager
# or
cd apps/planning-demo && pnpm dev # the planning demo
# or
cd apps/subagent-demo && pnpm dev # the subagent orchestration demo
# or
cd apps/mcp-demo && pnpm dev # the MCP adapter showcaseapps/bundled-demo, apps/planning-demo, and apps/subagent-demo all import
@forgewisp/bundled-tools via the workspace symlink to its dist/, and
apps/mcp-demo imports @forgewisp/mcp the same way, so build (or dev-watch)
the relevant package first:
pnpm --filter @forgewisp/bundled-tools build
pnpm --filter @forgewisp/mcp build # only needed for apps/mcp-demoEach demo will prompt for an LLM endpoint, model, and optional API key. It
persists these to localStorage under forgewisp.demo.config and reuses them on
reload; clear that key (or use the config overlay) to reconfigure.
import { createAgent } from '@forgewisp/core';
const agent = createAgent({
llmEndpoint: 'https://api.openai.com/v1/chat/completions',
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-4o',
systemPrompt: 'You manage tasks. Use the provided tools.',
onConfirmRequired: async (call) => {
return window.confirm(`${call.functionName}: ${JSON.stringify(call.args)}`);
},
audit: {
onEvent: (event) => console.log('[audit]', event),
},
streaming: {
reasoning: { mode: 'tag-based', tag: 'thinking' },
onTextChunk: (chunk) => appendToUI(chunk),
onReasoningChunk: (chunk) => appendReasoningToUI(chunk),
},
});
agent.registerFunction({
name: 'listTasks',
description: 'List all tasks.',
riskTier: 'read',
parameters: { type: 'object', properties: {}, required: [] },
handler: async () => tasks,
});
agent.registerFunction({
name: 'deleteTask',
description: 'Delete a task by id.',
riskTier: 'destructive',
parameters: {
type: 'object',
properties: { id: { type: 'integer', minimum: 1 } },
required: ['id'],
},
handler: async ({ id }) => { tasks = tasks.filter((t) => t.id !== id); },
});
const result = await agent.run('Delete task 2 and list the rest.');The public surface is intentionally small — createAgent, createSubagentTool,
and defineToolSet, plus the types they expose. Every config field is documented
in packages/core/src/types.ts; the essentials:
agent.run(message, options?)—optionsis{ signal?: AbortSignal; history?: ChatMessage[] }.historylets you warm the conversation with prior turns and accepts only user/assistant text (tool plumbing is not exposed, so callers can't build transcripts the API would reject).agent.registerFunction(def)/agent.deregisterFunction(name)/agent.clearAuditLog(). Registering awrite/destructivetool throws at registration time ifonConfirmRequiredis not configured.agent.registerToolSet(set)— register a namedToolSet(built withdefineToolSet) in one call.- Tool handlers receive
(args, context?)wherecontextis aToolContext({ signal?: AbortSignal }) carrying the parent run'sAbortSignal; long-running handlers can use it to abort. The second arg is optional, so existing one-arg handlers keep working. createSubagentTool(config)— build aspawnSubagenttool that delegates a sub-task to a fresh child agent and returns a trimmed result. Seepackages/corefor the full config/args/result shapes and the risk-tier rationale.streaming.onMalformedChunk— fires when an SSEdata:line can't be parsed as JSON; the stream continues.auditconfig block:maxEvents(default 1000, oldest dropped when exceeded),onEvent(called after every event), andredact(applied to each event before storage and beforeonEvent).- Defaults:
requestTimeoutMs60000 (set 0 to disable),maxToolRounds10.
Forgewisp runs in the browser and talks directly to an OpenAI-compatible
endpoint. Do not ship your provider API key in client-side code. Any key
embedded in browser code is extractable and will be abused. In production,
route requests through a proxy you control that injects the key server-side,
and point llmEndpoint at that proxy. LiteLLM
is a good fit since it speaks the OpenAI-compatible API Forgewisp already uses.
Minimal litellm_config.yaml exposing a model under a virtual name and
protecting it with a master key:
model_list:
- model_name: gpt-4o # the name your client sends
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY # kept on the server, never shipped
input_cost_per_token: 0.000005
output_cost_per_token: 0.000015
litellm_settings:
drop_params: true
request_timeout: 120
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY # required when proxy is on the webStart the proxy (pip install 'litellm[proxy]'):
export OPENAI_API_KEY=sk-...
export LITELLM_MASTER_KEY=sk-...
litellm --config litellm_config.yaml --port 4000Put it behind your own domain/TLS (e.g. https://your-proxy.example.com) and,
if it is internet-facing, lock it down with auth, rate limits, and CORS scoped
to your app's origin. Then point Forgewisp at the proxy instead of the provider:
const agent = createAgent({
llmEndpoint: 'https://your-proxy.example.com/v1/chat/completions',
// If your proxy requires the LiteLLM master key (or a virtual key you minted
// for this client), pass it as apiKey. It is *not* your provider key — that
// stays on the server — but treat it as a secret of its own and rotate it.
apiKey: 'sk-litellm-virtual-key-for-this-client',
model: 'gpt-4o', // matches model_name in litellm_config.yaml
// ...
});apiKey is fine for local development and the demo (where the key never leaves
your machine); in a deployed app prefer a short-lived virtual key over the
proxy's master key.
- Node.js ≥ 18, pnpm ≥ 9.
- Turborepo for task orchestration.
- tsup for builds (ESM, CJS, IIFE) —
@forgewisp/coreshipsdist/index.{mjs,cjs,global.js}+.d.ts; its only runtime dependency isajv.@forgewisp/mcpships ESM + CJS only and adds@modelcontextprotocol/sdkas its sole runtime dep (deliberately kept out of core). - Vitest for tests.
- ESLint + Prettier for code quality.
CI (.github/workflows/ci.yml) runs format:check, lint, typecheck,
build, and test on Node 20 with pnpm install --frozen-lockfile. Releases
are tag-driven (v*): the workflow verifies the tag matches all three
package versions (packages/core, packages/bundled-tools, and
packages/mcp — they version in lockstep), then publishes @forgewisp/core,
@forgewisp/bundled-tools, and @forgewisp/mcp to npm in that order with
provenance and creates a GitHub release. Bump all three package versions
alongside a release tag.
MIT