Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions scaffolds/maddox-think/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Alchemy / Cloudflare deploy credentials
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=

# Slack application
SLACK_SIGNING_SECRET=
SLACK_BOT_TOKEN=

# Shared company memory
GBRAIN_DATABASE_URL=

# Model provider. Start with Workers AI; add provider API keys only when selected.
MODEL_PROVIDER=workers-ai
PRIMARY_MODEL=@cf/moonshotai/kimi-k2.7-code
FALLBACK_MODEL=@cf/openai/gpt-oss-120b

# Optional integrations, introduced component-by-component
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
GITHUB_INSTALLATION_ID=
LINEAR_API_KEY=
SENTRY_AUTH_TOKEN=
8 changes: 8 additions & 0 deletions scaffolds/maddox-think/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.alchemy/
.dev.vars
.env
node_modules/
coverage/
dist/
.wrangler/
pnpm-debug.log*
6 changes: 6 additions & 0 deletions scaffolds/maddox-think/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.alchemy/
.wrangler/
node_modules/
coverage/
dist/
pnpm-lock.yaml
40 changes: 40 additions & 0 deletions scaffolds/maddox-think/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Agent Guide

This repository is a Cloudflare-native Think application whose business logic is written with Effect.

## Non-negotiable boundaries

- `MaddoxConversationAgent` must extend `Think`; keep the class thin.
- Think callbacks may call Effect programs through `MaddoxRuntime`; they do not contain integration logic.
- Effect Schema is the authority for domain and external data. Zod exists only at the AI SDK / Think tool boundary.
- Every external response is decoded before it crosses an adapter boundary.
- Domain failures are `Schema.TaggedErrorClass` values. Do not use message-string matching.
- Never log, persist, return to the model, or place in workspace files any credential.
- Read-only capabilities are Think tools. External mutations are Think Actions.
- Every mutation has a stable idempotency key and explicit permission.
- Durable Objects, Workflows, R2, D1, AI bindings, secrets, routes, and domains are declared only in `alchemy.run.ts` or modules imported by it.
- No Wrangler configuration is committed.
- Think SQLite is thread-local runtime state. Shared company knowledge belongs behind the GBrain service.
- Local Hermes is an import source, never a runtime dependency.
- No automatic executable skill creation in the first release.

## Effect conventions

- Use `Context.Service` for application capabilities.
- Put production Layers in `live.ts`; deterministic implementations in `test.ts`.
- Keep provider SDKs behind named adapter packages.
- Decode configuration once at the runtime boundary.
- Use the Effect Clock instead of `Date.now()` in domain code.
- Use `Effect.retry` only with an explicit retry policy and retryable tagged failures.
- Use `Effect.log*` with structured, redacted annotations.
- Do not rely on finalizers for correctness across Durable Object eviction.

## Definition of done for a component

1. Public contracts and errors exist.
2. A deterministic Test Layer exists.
3. The production adapter decodes all unknown data.
4. Authorization and idempotency behavior are tested where relevant.
5. Failure output is safe for Slack and model context.
6. `pnpm check` passes.
7. The component's implementation-plan checklist is updated.
71 changes: 71 additions & 0 deletions scaffolds/maddox-think/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Maddox Think

A standalone Cloudflare-native company agent for Card Madness.

This scaffold intentionally does **not** migrate or depend on the existing cloud-hosted Hermes deployment. It starts clean and imports only selected identity, skills, and durable knowledge from the local Hermes instance through an explicit, reviewable import bundle.

## Architectural thesis

- **Think owns the conversational runtime.** One `MaddoxConversationAgent` Durable Object is addressed per Slack thread.
- **Effect owns application behavior.** Contracts, policies, integrations, authorization, memory, tools, actions, import logic, retries, and errors are Effect services.
- **Alchemy owns all infrastructure.** There is no hand-maintained Wrangler configuration.
- **Reads are tools; writes are actions.** Side effects use Think Actions with authorization, idempotency, and approval.
- **A real computer is optional.** Repository builds, browser work, and native processes use an on-demand sandbox in a later slice.
- **Company memory is separate from chat history.** Think SQLite stores thread-local state; GBrain remains the shared, provenance-aware company knowledge store.

## Status

This is a high-level scaffold, not a deploy-ready release. The first implementation slice is deliberately small:

1. Alchemy deploys one Worker and the Think Durable Object namespace.
2. Slack ingress verifies signatures and derives a stable thread identity.
3. The Worker durably submits a message to the thread's Think agent.
4. The agent uses a deterministic fake model in tests and one configured model in development.
5. The response delivery boundary is represented by a typed Effect service.

See:

- [Architecture](docs/architecture.md)
- [Component plan](docs/implementation-plan.md)
- [Local Hermes import](docs/hermes-import.md)
- [Think and Effect boundary ADR](docs/decisions/0001-think-effect-boundary.md)
- [Conversation identity ADR](docs/decisions/0002-thread-per-agent.md)

## Intended repository

Create a new private repository, preferably:

```text
card-madness/maddox-think
```

Copy the contents of this directory to the root of that repository. Do not merge the scaffold into Card Madness, the current Maddox deployment, or the Effect template itself.

## Local commands

The dependency versions are intentionally pinned to the currently reviewed Think and Alchemy releases. Re-review both before the first production deploy because they are still pre-stable.

```bash
pnpm install
pnpm typecheck
pnpm test
pnpm alchemy:dev
```

## Repository map

```text
apps/
worker/ Cloudflare entry module and Think Durable Object
packages/
contracts/ Effect Schema authorities and tagged domain values
core/ Application services, policies, and deterministic doubles
runtime/ Thin Think-to-Effect adapter and layer construction
skills/
company-operating-system/
docs/
decisions/
alchemy.run.ts Alchemy stack and typed Cloudflare bindings
```

The implementation plan adds packages only when a component has a real boundary. Avoid turning this into a large generic platform before the Slack-to-Think vertical slice works.
75 changes: 75 additions & 0 deletions scaffolds/maddox-think/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";

import type { MaddoxConversationAgent } from "./apps/worker/src/maddox-agent.ts";

const MaddoxWorker = () =>
Effect.gen(function* () {
const skills = yield* Cloudflare.R2.Bucket("MaddoxSkills");
const artifacts = yield* Cloudflare.R2.Bucket("MaddoxArtifacts");
const registry = yield* Cloudflare.D1.Database("MaddoxRegistry", {
migrations: "./migrations",
});

return yield* Cloudflare.Worker("MaddoxThink", {
main: "./apps/worker/src/index.ts",
compatibility: {
date: "2026-08-25",
flags: ["nodejs_compat"],
},
env: {
AI: Cloudflare.Workers.AI(),
ARTIFACTS: artifacts,
FALLBACK_MODEL: Config.string("FALLBACK_MODEL").pipe(
Config.withDefault("@cf/openai/gpt-oss-120b"),
),
GBRAIN_DATABASE_URL: Config.redacted("GBRAIN_DATABASE_URL"),
MODEL_PROVIDER: Config.string("MODEL_PROVIDER").pipe(
Config.withDefault("workers-ai"),
),
MaddoxConversationAgent:
Cloudflare.DurableObject<MaddoxConversationAgent>(
"MaddoxConversationAgent",
{ className: "MaddoxConversationAgent" },
),
PRIMARY_MODEL: Config.string("PRIMARY_MODEL").pipe(
Config.withDefault("@cf/moonshotai/kimi-k2.7-code"),
),
REGISTRY: registry,
SKILLS: skills,
SLACK_BOT_TOKEN: Config.redacted("SLACK_BOT_TOKEN"),
SLACK_SIGNING_SECRET: Config.redacted("SLACK_SIGNING_SECRET"),
},
observability: {
enabled: true,
logs: {
enabled: true,
invocationLogs: true,
},
traces: {
enabled: true,
headSamplingRate: 1,
},
},
});
});

export type MaddoxEnv = Cloudflare.InferEnv<ReturnType<typeof MaddoxWorker>>;

export default Alchemy.Stack(
"MaddoxThink",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const worker = yield* MaddoxWorker();

return {
workerName: worker.workerName,
url: worker.url,
};
}),
);
12 changes: 12 additions & 0 deletions scaffolds/maddox-think/apps/worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@maddox/worker",
"private": true,
"type": "module",
"dependencies": {
"@cloudflare/think": "0.16.0",
"@maddox/contracts": "workspace:*",
"@maddox/runtime": "workspace:*",
"agents": "0.21.0",
"workers-ai-provider": "^4.0.0"
}
}
86 changes: 86 additions & 0 deletions scaffolds/maddox-think/apps/worker/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { getAgentByName, routeAgentRequest } from "agents";

import type { MaddoxEnv } from "../../../alchemy.run.ts";
import { decodeSlackEventRequest, SlackRequestRejected } from "@maddox/contracts";
import { makeMaddoxRuntime, type MaddoxRuntime } from "@maddox/runtime";

export { MaddoxConversationAgent } from "./maddox-agent.ts";

const notFound = () => new Response("Not found", { status: 404 });

let ingressRuntime: MaddoxRuntime | undefined;

const getIngressRuntime = (env: MaddoxEnv): MaddoxRuntime =>
(ingressRuntime ??= makeMaddoxRuntime(env));

export default {
async fetch(
request: Request,
env: MaddoxEnv,
executionContext: ExecutionContext,
): Promise<Response> {
const routed = await routeAgentRequest(request, env);
if (routed) {
return routed;
}

const url = new URL(request.url);

if (url.pathname === "/healthz") {
return Response.json({ status: "ok" });
}

if (url.pathname !== "/webhooks/slack" || request.method !== "POST") {
return notFound();
}

const runtime = getIngressRuntime(env);

try {
const event = await runtime.runPromise(
decodeSlackEventRequest(request, env.SLACK_SIGNING_SECRET),
);

if (event.kind === "url-verification") {
return Response.json({ challenge: event.challenge });
}

const agent = await getAgentByName(
env.MaddoxConversationAgent,
event.conversationId,
);

await agent.submitMessages(
[
{
id: event.eventId,
role: "user",
parts: [{ type: "text", text: event.text }],
},
],
{
idempotencyKey: event.idempotencyKey,
metadata: {
channelId: event.channelId,
slackEventId: event.eventId,
teamId: event.teamId,
threadTs: event.threadTs,
userId: event.userId,
},
},
);

return new Response(null, { status: 200 });
} catch (error) {
if (error instanceof SlackRequestRejected) {
return Response.json(
{ error: error.reason },
{ status: error.statusCode },
);
}

executionContext.waitUntil(runtime.reportDefect(error));
return Response.json({ error: "internal_error" }, { status: 500 });
}
},
} satisfies ExportedHandler<MaddoxEnv>;
48 changes: 48 additions & 0 deletions scaffolds/maddox-think/apps/worker/src/maddox-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Think, type ThinkScheduledTasks } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";

import type { MaddoxEnv } from "../../../alchemy.run.ts";
import { makeMaddoxRuntime } from "@maddox/runtime";

export class MaddoxConversationAgent extends Think<MaddoxEnv> {
override workspaceBash = false;

private readonly app = makeMaddoxRuntime(this.env);

override getModel() {
return createWorkersAI({ binding: this.env.AI })(this.env.PRIMARY_MODEL);
}

override getSystemPrompt(): string {
return [
"You are Maddox, the Card Madness company agent.",
"Be direct, evidence-seeking, and explicit about uncertainty.",
"Use read-only tools freely when relevant.",
"Use actions for every external mutation.",
"Never expose credentials or internal implementation secrets.",
].join("\n");
}

override getTools() {
return this.app.thinkTools();
}

override getActions() {
return this.app.thinkActions();
}

override getDefaultTimezone(): string {
return "America/New_York";
}

override getScheduledTasks(): ThinkScheduledTasks {
return {
"weekly-reflection": {
schedule: "every week on monday at 09:00",
handler: async () => {
await this.app.runPromise(this.app.runReflection());
},
},
};
}
}
Loading
Loading