Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
5088aee
feat(v2): scaffold module-sdk + tool/skill/module registries + tool_c…
parag May 8, 2026
0b1608b
feat(v2): zod-validated tool dispatcher + POST /api/tools/:fullName r…
parag May 8, 2026
c347054
feat(v2): skills + tool-catalog context providers wired into pipeline
parag May 8, 2026
ea2569f
feat(v2): framework module — tools, skills, factory pattern for built…
parag May 8, 2026
a5fd6d7
feat(v2): memory + drive built-in modules wrapping v1 providers
parag May 8, 2026
8a23243
feat(v2): workflow + inbox built-in modules
parag May 8, 2026
5c8fc68
docs(v2): update CLAUDE.md with v2 architecture, add BUILD-A-MODULE.m…
parag May 8, 2026
0d03c5c
feat(v2): slack connector module — wraps SlackClient as Module
parag May 8, 2026
0114738
feat(v2): google connector module — gmail.* + calendar.* tools
parag May 8, 2026
e24c4ba
feat(v2): /health surfaces module summary; ship session 1 progress doc
parag May 8, 2026
1ba2002
feat(v2): hebbs-crm hybrid module — schema + tools + skill
parag May 8, 2026
acfb154
feat(v2): copilot module — start_session tool + skill
parag May 8, 2026
6573703
feat(v2): capability resolution + module_installs table
parag May 8, 2026
d305415
feat(v2): admin endpoints — modules / tools / tool-calls audit
parag May 8, 2026
739f184
docs(v2): final session-1 progress — 10 phases done, 39 v2 tests passing
parag May 8, 2026
2144c26
feat(v2): lifecycle hooks runtime + per-tenant install state
parag May 8, 2026
992b842
feat(v2): admin UI panels — modules / tool catalog / tool calls
parag May 8, 2026
3ddd779
feat(v2): triage capability module with dependsOn capability resolution
parag May 8, 2026
d5615a4
feat(v2): workflow.run tool walks DAG and dispatches per-block tools
parag May 8, 2026
564225a
feat(v2): workflow.run control-flow blocks (condition, for_each, dela…
parag May 8, 2026
1263e42
feat(v2): SKILL.md disk loading with YAML frontmatter parsing
parag May 8, 2026
c8d13ee
feat(v2): Module.schema migration runtime — apply on install, rollbac…
parag May 8, 2026
dafbca0
feat(v2): workflow blocks palette in Settings — 5 control-flow + tool…
parag May 8, 2026
82547c8
feat(v2): config.v2Only flag — disable v1 routes + providers as safe …
parag May 8, 2026
88d47ef
feat(v2): parity test suite + agents.create reportsTo defaulting
parag May 8, 2026
3f74dcc
docs(v2): MIGRATION-V1-TO-V2.md + final session progress doc
parag May 8, 2026
7aeedf3
feat(dev): register all v2 modules in dev server, BORINGOS_V2_ONLY en…
parag May 8, 2026
2984881
feat(dev): v2-only is the default; BORINGOS_KEEP_V1=true to opt back in
parag May 8, 2026
18d3c5c
fix(v2): google module — pass token string to GmailClient + OAuth ref…
parag May 8, 2026
3b100b3
feat(v2): framework.agents.wake tool + auto-wake on task assignment
parag May 8, 2026
98277c0
fix(v2): workflow.run templates support interpolation within strings …
parag May 8, 2026
7b1e2b4
feat(v1-deletion): remove v1 routes + 7 v1 providers + v2Only flag
parag May 8, 2026
1c207c2
feat(shell): rebuild Copilot screen on /api/admin/tasks (v1 copilot r…
parag May 8, 2026
a687fec
feat(shell): rebuild Workflows screen on v2 block schema with tool pa…
parag May 8, 2026
52c0acb
feat(v1-deletion): delete @boringos/workflow + @boringos/workflow-ui …
parag May 8, 2026
660abd9
feat(v1-deletion): delete @boringos/connector framework; extract OAut…
parag May 8, 2026
6522014
fix(v2): drop undefined params from tool_calls audit + OAuth refresh …
parag May 8, 2026
4c57c41
fix+chore: reverse-sync OAuth refresh-on-401; commit pending shell Se…
parag May 8, 2026
329a610
fix(connectors): mount listing handler at /status alongside /connecto…
parag May 8, 2026
b1938d0
fix(v2): dual-mode auth on /api/tools (callback JWT or session bearer…
parag May 8, 2026
72c2642
feat(inbox): forward-sync ticker ingests new Gmail messages into inbo…
parag May 8, 2026
f7ec566
feat(inbox): forward-sync fans out to triage + replier agents directl…
parag May 8, 2026
be072dc
feat(shell+workflows): refactor Workflows screen into modular compone…
parag May 9, 2026
b8b3e46
fix(tests): update test expectations to match actual implementation b…
parag May 9, 2026
afe4d85
feat(handoff): next_actor state machine — agent runs auto-flip task t…
parag May 9, 2026
db4255a
fix(handoff): /comments restores wake-on-user-comment + flips next_ac…
parag May 9, 2026
73aea04
feat(ux): default new-task assignee to Chief of Staff (looked up by r…
parag May 9, 2026
78cc05f
feat(time): add current-time context provider — every agent wake gets…
parag May 9, 2026
6ed8946
feat(copilot): name sessions from first message — heuristic title on …
parag May 9, 2026
404721b
feat(shell): cabinet rebuild + IA cleanup + settings manifest + theme…
parag May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
303 changes: 303 additions & 0 deletions BUILD-A-MODULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
# Build a Module — v2 quickstart

> **Status:** Working starter. For canonical field-by-field
> reference see [`MODULES.md`](MODULES.md), [`TOOLS.md`](TOOLS.md),
> and [`SKILLS.md`](SKILLS.md). This file is the practical
> minimum: what works on `branch_modules_skills` today.

This file teaches you to write a v2 Module — the universal
component shape that replaces v1's connector / app / plugin
trio. A Module is a manifest of skills + tools the agent can
read and call. The framework wires the rest.

---

## What you need

- TypeScript / Node 22+
- `pnpm install` at the repo root
- Be on `branch_modules_skills` (v2 lives there until cutover)

---

## The minimal Module

A Module manifest is a plain object. Here's the smallest possible
one:

```typescript
import { z } from "@boringos/module-sdk";
import type { Module } from "@boringos/module-sdk";

export const helloModule: Module = {
id: "hello",
name: "Hello",
version: "0.1.0",
description: "Demo module — one tool, one skill",

skills: [
{
id: "hello",
source: "module",
body: "Use `hello.greet` to greet someone by name. " +
"It's a no-op example — useful for verifying " +
"your prompt sees v2 modules.",
},
],

tools: [
{
name: "greet",
description: "Greet someone by name",
inputs: z.object({ name: z.string() }),
async handler({ name }) {
return { ok: true, result: { greeting: `hello, ${name}` } };
},
},
],
};
```

Register it on a BoringOS host:

```typescript
import { BoringOS } from "@boringos/core";
import { helloModule } from "./hello-module.js";

const app = new BoringOS({});
app.module(helloModule);
await app.listen(3000);
```

That's it. The agent's prompt now includes:

- A `## Skills` section with the `### hello` block
- A `## Available tools` section listing `hello.greet`

The agent can call it with:

```bash
curl -X POST http://localhost:3000/api/tools/hello.greet \
-H "Authorization: Bearer $BORINGOS_CALLBACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "world"}'

# {"ok": true, "result": {"greeting": "hello, world"}}
```

---

## When to use a `ModuleFactory` instead

The inline form above works when your Module doesn't need access
to framework services (DB, memory provider, etc.). When you do,
pass a factory function instead — the framework calls it after
boot with the deps:

```typescript
import type { ModuleFactory } from "@boringos/module-sdk";
import type { Db } from "@boringos/db";

export const myCrmModule: ModuleFactory = (deps) => {
const db = deps.db as Db;

return {
id: "my-crm",
name: "My CRM",
version: "0.1.0",
description: "...",
tools: [
{
name: "list_deals",
description: "List all deals for the tenant",
inputs: z.object({}),
async handler(_input, ctx) {
const rows = await db
.select()
.from(/* your schema */)
.where(/* tenantId = ctx.tenantId */);
return { ok: true, result: { deals: rows } };
},
},
],
};
};

// Register it the same way:
app.module(myCrmModule);
```

`ModuleFactoryDeps` exposes `db`, `memory`, `drive`, `engine`,
`workflowEngine`. Cast to your concrete types.

---

## Anatomy of a Tool

```typescript
{
name: "create_deal", // local name; full URL becomes <module>.create_deal
description: "Create a deal", // shown to the agent in the catalog
inputs: z.object({ // Zod schema — validated before handler runs
contactId: z.string().uuid(),
amount: z.number().positive(),
stage: z.enum(["new", "qualified", "won", "lost"]).optional(),
}),
output: z.object({ // optional — output schema for return values
dealId: z.string(),
}),
async handler(input, ctx) { // input is z.infer<typeof inputs>; ctx is ToolContext
// ctx.tenantId, ctx.agentId, ctx.runId, ctx.taskId, ctx.invokedBy

if (/* business rule fails */) {
return {
ok: false,
error: {
code: "invalid_input",
message: "Contact does not exist",
retryable: false,
},
};
}

const dealId = await /* do the work */;
return { ok: true, result: { dealId } };
},
}
```

### Error model

Tools return either `{ ok: true, result }` or `{ ok: false, error }`.

`error.code` is one of:
- `invalid_input` — schema validation failed (the framework returns this automatically when Zod rejects)
- `not_found` — referenced entity doesn't exist
- `permission_denied` — caller can't do this
- `upstream_unavailable` — 3rd-party API is down or misbehaving
- `rate_limited` — caller exceeded a quota
- `conflict` — concurrent write conflict
- `internal` — handler threw an uncaught error (the framework converts these and returns 500)

`error.retryable` tells the agent whether to retry. The framework
SKILL teaches the agent the retry policy.

### What the dispatcher does for you

Before your handler runs:
- Verifies the JWT (agent calls only — internal callers skip this)
- Looks up the tool by full name; 404 if missing
- Validates inputs against your Zod schema; 400 if invalid

After your handler returns:
- Wraps the result in the right HTTP status (200 for ok or business error, 500 for thrown)
- Writes a `tool_calls` audit row (tenant, tool, inputs, result, duration, status)

You write business logic. The framework handles the rest.

---

## Anatomy of a Skill

A Skill is markdown injected into the agent's prompt. Today they
live as inline `Skill` objects on the Module manifest. In Phase 6+
they move to literal `SKILL.md` files in your package, with
frontmatter for metadata.

```typescript
{
id: "crm", // unique within the module
source: "module", // how it was loaded — "module" for in-package
body: `Use the CRM tools to ... [markdown content]`,
priority: 100, // ordering in the prompt; lower = earlier
appliesTo: (event) => // optional gating
event.agentRole === "sales-rep",
requires: ["crm.list_deals"], // (future) flag drift if this tool is missing
}
```

Priority ranges:
- `50` — framework-level (tool-protocol, approvals, when-stuck)
- `60-90` — module-shipped skills
- `200+` — agent persona / instructions
- `400` — tenant override

Lower priority appears EARLIER in the prompt. Higher priority
appears closer to the task — more influence on agent behavior.

---

## Testing your Module

```typescript
import { describe, it, expect } from "vitest";
import { createToolRegistry, dispatch } from "@boringos/agent";
import { z } from "@boringos/module-sdk";
import { helloModule } from "./hello-module.js";

describe("hello module", () => {
it("greets via the dispatcher", async () => {
const tools = createToolRegistry();
for (const tool of helloModule.tools ?? []) {
tools.register(helloModule.id, tool);
}

const out = await dispatch(
{ registry: tools },
"hello.greet",
{ name: "world" },
{
tenantId: "t1",
agentId: "a1",
runId: "r1",
invokedBy: "agent",
},
);

expect(out.status).toBe(200);
expect(out.result.ok).toBe(true);
expect(out.result.result.greeting).toBe("hello, world");
});
});
```

For HTTP-level testing, see the existing patterns in
`tests/v2-http.test.ts` and `tests/v2-framework-module.test.ts`.

---

## What's NOT in this starter

The full Module manifest supports much more than what's shown
above. Below is the eight-dimensional surface — items marked with
🔜 ship in later phases of `task_12`:

| Field | Status |
|---|---|
| `skills` | ✅ inline; 🔜 SKILL.md files in Phase 6 |
| `tools` | ✅ |
| `dependsOn` / `provides` | 🔜 Phase 9 (capability resolution) |
| `schema` (Drizzle migrations, prefixed `<id>__`) | 🔜 Phase 8 (CRM port) |
| `ui` (screens, panels, settings) | 🔜 Phase 10 |
| `workflows` (default seeded) | 🔜 Phase 9 |
| `agents` (default seeded) | 🔜 Phase 9 |
| `routines` (cron / event / webhook) | 🔜 Phase 9 |
| `webhooks` (inbound HTTP) | 🔜 Phase 7 connector polish |
| `oauth` | 🔜 Phase 7 connector polish |
| `lifecycle.{onInstall, onUninstall, onTenantCreate}` | 🔜 Phase 5 polish |

---

## Next steps after this guide

1. Read [`docs/blockers/task_12_greenfield_rebuild.md`](docs/blockers/task_12_greenfield_rebuild.md)
end-to-end if you'll be authoring or porting Modules.
2. Look at `packages/@boringos/core/src/v2-modules/framework.ts`
for a complete real Module — 9 tools, 3 skills, full DB
integration.
3. The other built-ins (`memory.ts`, `drive.ts`, `workflow.ts`,
`inbox.ts`) are tighter examples of single-purpose modules.
4. The CRM port (`task_12` Phase 8) will be the first hybrid
Module exercising every dimension — schema, UI, default
workflows, default agents. That's the canonical guide
`task_13` rewrites this file around.
Loading
Loading