History

server

Rhys Sullivan committed 9 months ago

Commit e00332d1 parent ef9b87520 files · +1670 −728Browse files

Unified · Split

20 files
  1. PLUGIN_ARCHITECTURE.md +662 −347
  2. apps/discord-bot/package.json +2 −0
  3. apps/discord-bot/src/agent-loop.ts +1 −1
  4. apps/discord-bot/src/format-response.test.ts +9 −82
  5. apps/discord-bot/src/format-response.ts +4 −259
  6. apps/discord-bot/src/gateway-client.ts +35 −0
  7. apps/discord-bot/src/index.tsx +7 −30
  8. apps/gateway/package.json +7 −1
  9. apps/gateway/src/agent-loop.ts +199 −0
  10. apps/gateway/src/anthropic-auth.ts +133 −0
  11. apps/gateway/src/anthropic-provider.ts +284 −0
  12. apps/gateway/src/calendar-store.ts +32 −0
  13. apps/gateway/src/code-typecheck.ts +87 −0
  14. apps/gateway/src/index.ts +70 −6
  15. apps/gateway/src/tools.ts +22 −0
  16. bun.lock +73 −2
  17. package.json +17 −0
  18. packages/core/package.json +1 −0
  19. packages/core/src/agent-rpc.ts +24 −0
  20. packages/core/src/index.ts +1 −0
PLUGIN_ARCHITECTURE.md+662−347
@@ -15,30 +15,263 @@ We need a plugin system that makes tools **installable, discoverable, and config · 14 unmodified lines
15 15
16---16---
17 17
18## Reference Analysis
19
20We studied two production systems before designing this:
21
22### OpenCode (`.reference/opencode/`)
23
24OpenCode's approach is the primary inspiration. Key patterns worth adopting:
25
261. **Convention directories**: `.opencode/tool/`, `.opencode/plugin/`, `.opencode/skill/` — drop a `.ts` file in and it just works. No manifest file needed for simple tools.
272. **Auto-dependency management**: When `.opencode/` contains tools or plugins, OpenCode auto-generates a `package.json` with `@opencode-ai/plugin` and runs `bun install`. Zero friction.
283. **Plugin = function that returns hooks**: `Plugin = (input: PluginInput) => Promise<Hooks>`. Clean, no class ceremony.
294. **Tool definition via SDK**: `tool({ description, args: z.object({...}), execute(args) })` — zod schema inline, description as a string. Simple.
305. **File naming = tool ID**: Default export of `github-pr-search.ts` becomes tool `github-pr-search`. Named exports become `filename_exportname`.
316. **Multiple config sources**: Global (`~/.config/opencode/`), home (`~/.opencode/`), project (`.opencode/`), remote (`.well-known/opencode`). Deep merge with priority ordering.
327. **Bun-native loading**: Uses `import()` directly (Bun handles TypeScript). Also uses `Bun.Glob` for file discovery.
338. **Skills as Markdown**: `SKILL.md` files with YAML frontmatter. Separate from tools — skills are context/instructions, tools are callable functions.
34
35### OpenClaw (`.context/openclaw/`)
36
37More complex but some patterns worth noting:
38
391. **jiti for TypeScript loading**: Works across Node and Bun, handles SDK aliasing at import time.
402. **Plugin manifest (`openclaw.plugin.json`)**: JSON Schema for config validation without executing plugin code.
413. **Layered precedence**: Config > Workspace > Global > Bundled, with first-match-wins deduplication.
424. **Registration API pattern**: `register(api)` where `api.registerTool()`, `api.registerHook()`, etc. Very extensible but heavyweight for MVP.
43
44### What We're Taking
45
46From OpenCode:
47- Convention directories (`.openassistant/tools/`, `.openassistant/plugins/`)
48- File-as-tool pattern (drop `.ts` in `tools/`, it just works)
49- Bun-native `import()` for module loading
50- Zod schemas on tool definitions (args + description)
51- Auto-dependency management for `.openassistant/` directories
52- Simple plugin function pattern
53
54From OpenClaw:
55- Plugin manifest for config schema validation (but optional, not required)
56- Config interpolation (`${ENV_VAR}`) for secrets
57
58Avoiding from both:
59- OpenClaw's heavyweight registration API (too many `registerX()` methods for MVP)
60- Over-engineered config merging (keep it simple: project > global)
61
62---
63
18## Design Principles64## Design Principles
19 65
201. **Config-file-driven.** One config file is the source of truth for what's loaded.661. **Convention over configuration.** Drop a `.ts` file in `tools/`, it becomes a tool. No manifest needed for simple cases.
212. **No runtime magic.** Discovery is explicit scan, not auto-import. No decorators, no file-convention routing.672. **Config when you need it.** Full plugins with config schemas exist for complex integrations, but aren't required.
223. **Tools are the plugin unit.** MVP plugins = tool packages. Hooks, channels, services come later.683. **Tools are the plugin unit.** MVP plugins = tool packages. Hooks, channels, services come later.
234. **TypeScript-native.** Plugins are authored in TypeScript, loaded via `jiti` (or Bun's native loader). No build step required for plugin authors.694. **TypeScript-native.** Bun loads `.ts` directly. No build step. No jiti (Bun-only for MVP).
245. **Monorepo-friendly.** Works whether your plugin is a workspace package, a path on disk, or an npm install.705. **Monorepo-friendly.** Works whether your tool is a file in `.openassistant/tools/`, a workspace package, or an npm install.
256. **Minimal surface.** Plugin API is `defineTool()` + a manifest. That's it for MVP.716. **Zod schemas are the contract.** Input schemas enable type declaration generation, validation, and documentation — all from one source.
727. **Effect is an internal.** Tool authors never see Effect. The `run` function is a plain `async` function. The core runtime wraps it in Effect internally.
738. **Approval is the only policy knob.** No `kind`/`risk`/`type` classification. The tool author says `"auto"` or `"required"`. Operators can override via config.
74
75---
76
77## `defineTool()` — The Only API
78
79There's one function to define a tool: `defineTool()`. No tiers, no `tool()` vs `defineTool()` split.
80
81```ts
82import { defineTool, z } from "@openassistant/sdk";
83
84export default defineTool({
85 description: "Get current weather for a location",
86 approval: "auto",
87 args: z.object({
88 location: z.string().describe("City name or coordinates"),
89 }),
90 run: async (input) => {
91 const res = await fetch(`https://wttr.in/${input.location}?format=j1`);
92 return res.json();
93 },
94});
95```
96
97That's it. `run` is an async function. No Effect. No special return types. Throw to fail, return to succeed.
98
99### Full Signature
100
101```ts
102function defineTool<TArgs extends z.ZodRawShape>(options: {
103 /** Human description of what this tool does (shown to the LLM) */
104 description: string;
105 /** Whether the tool auto-runs or requires human approval before execution */
106 approval: "auto" | "required";
107 /** Zod schema for the input arguments */
108 args: z.ZodObject<TArgs>;
109 /** The tool implementation. Just an async function. */
110 run: (input: z.infer<z.ZodObject<TArgs>>) => Promise<unknown>;
111 /** Optional: format the input for the approval UI / receipt preview */
112 previewInput?: (input: z.infer<z.ZodObject<TArgs>>) => string;
113 /** Optional: format the output for the receipt preview */
114 previewOutput?: (output: unknown) => string;
115}): ToolDefinition
116```
117
118`approval` is the only field that controls execution policy at the tool level. There's no `kind` / `risk` classification — the tool author decides whether their tool needs human sign-off or not. Operators can override this via config/policy later (force approval on auto tools, auto-approve required tools for trusted contexts, deny tools entirely).
119
120### What Happens Internally
121
122The core runner (`packages/core/src/codemode/runner.ts`) wraps the plain async `run` function in Effect at materialization time — in `createToolInvoker()`. Plugin authors never know or care.
123
124```ts
125// Inside createToolInvoker (core internal):
126const execution = await Effect.runPromiseExit(
127 Effect.tryPromise(() => definition.run(validatedInput))
128);
129```
130
131The current `defineTool()` in core takes `run: (input) => Effect.Effect<T, E>`. That signature changes to `run: (input) => Promise<T>`. The core runner wraps it. The existing `tools.ts` in discord-bot updates accordingly (just remove the `Effect.sync()` / `Effect.tryPromise()` wrappers from tool definitions).
132
133### Before and After
134
135**Before** (current `apps/discord-bot/src/tools.ts`):
136```ts
137import { defineTool } from "@openassistant/core";
138import { Effect } from "effect";
139
140defineTool({
141 kind: "write",
142 approval: "required",
143 run: (input: { title: string; startsAt: string }) =>
144 Effect.sync(() => calendarStore.update(input)), // Effect leaks into authoring
145 previewInput: (input) => `${input.title} @ ${input.startsAt}`,
146});
147```
148
149**After** (new API):
150```ts
151import { defineTool, z } from "@openassistant/sdk";
152
153defineTool({
154 description: "Update a calendar event",
155 approval: "required",
156 args: z.object({
157 title: z.string(),
158 startsAt: z.string(),
159 notes: z.string().optional(),
160 }),
161 run: async (input) => calendarStore.update(input),
162 previewInput: (input) => `${input.title} @ ${input.startsAt}`,
163});
164```
165
166Effect is gone from the authoring surface. `kind` is gone — `approval` is the only policy field. The `args` zod schema replaces the inline TypeScript type annotation on `run`'s parameter. `description` is now required.
167
168---
169
170## Where `defineTool()` Lives
171
172Two packages, two roles:
173
174| Package | Role | Contains |
175|---------|------|----------|
176| `packages/core` | **Runtime engine** | `ToolDefinition` type, `createCodeModeRunner()`, materialization, receipts, approval flow. Effect lives here. |
177| `packages/sdk` | **Authoring surface** | `defineTool()` function, `z` re-export, `PluginContext` type. This is what plugin authors import. No Effect. |
178
179The SDK's `defineTool()` creates a `ToolDefinition` object (with `_tag: "ToolDefinition"`). The core runtime consumes it. The SDK doesn't import Effect at all — it's a thin layer that produces the right shape.
180
181```ts
182// packages/sdk/src/define-tool.ts
183import { z } from "zod";
184
185export interface ToolDefinition {
186 readonly _tag: "ToolDefinition";
187 readonly description: string;
188 readonly approval: "auto" | "required";
189 readonly args: z.ZodObject<any>;
190 readonly run: (input: unknown) => Promise<unknown>;
191 readonly previewInput?: (input: any) => string;
192 readonly previewOutput?: (output: unknown) => string;
193}
194
195export function defineTool<TArgs extends z.ZodRawShape>(options: {
196 description: string;
197 approval: "auto" | "required";
198 args: z.ZodObject<TArgs>;
199 run: (input: z.infer<z.ZodObject<TArgs>>) => Promise<unknown>;
200 previewInput?: (input: z.infer<z.ZodObject<TArgs>>) => string;
201 previewOutput?: (output: unknown) => string;
202}): ToolDefinition {
203 return {
204 _tag: "ToolDefinition",
205 description: options.description,
206 approval: options.approval,
207 args: options.args,
208 run: options.run,
209 previewInput: options.previewInput,
210 previewOutput: options.previewOutput,
211 };
212}
213```
214
215The core's `createToolInvoker()` then does:
2161. Parse input through `definition.args` (zod validation)
2172. Call `definition.run(validatedInput)`
2183. Wrap in Effect for receipt tracking, approval flow, etc.
26 219
27---220---
28 221
29## What Is a Plugin?222## Standalone Tools vs Full Plugins
223
224### Standalone Tools (Zero Friction)
225
226A tool is a single `.ts` file in a convention directory. No `package.json`, no manifest.
227
228```
229.openassistant/
230 tools/
231 weather.ts
232 github-issues.ts
233```
234
235**`weather.ts`**:
236```ts
237import { defineTool, z } from "@openassistant/sdk";
238
239export default defineTool({
240 description: "Get current weather for a location",
241 approval: "auto",
242 args: z.object({
243 location: z.string().describe("City name or coordinates"),
244 }),
245 run: async (input) => {
246 const res = await fetch(`https://wttr.in/${input.location}?format=j1`);
247 return res.json();
248 },
249});
250```
251
252Rules:
253- **Default export** → tool ID = filename without extension (`weather`)
254- **Named exports** → tool ID = `filename.exportName` (`github_issues.list`)
30 255
31A plugin is a directory (or npm package) containing:256### Full Plugins (Config + Multiple Tools + Dependencies)
257
258A plugin is a directory with `package.json` (has `"openassistant"` key) and optionally `openassistant.json` for config schema.
32 259
33```260```
34my-plugin/261.openassistant/
35 package.json # Must have "openassistant" key262 plugins/
36 openassistant.json # Plugin manifest (metadata, config schema)263 posthog/
37 index.ts # Entry point, default exports a ToolTree or register function264 package.json
265 openassistant.json # Optional: config schema
266 index.ts
38```267```
39 268
40### `package.json` (minimal)269Or as an npm package:
270```bash
271bun add @openassistant/plugin-posthog
272```
41 273
274**`package.json`** (minimal):
42```json275```json
43{276{
44 "name": "@myorg/oa-posthog",277 "name": "@myorg/oa-posthog",
@@ -55,19 +288,12 @@ my-plugin/ · 10 unmodified lines
55}288}
56```289```
57 290
58The `"openassistant"` key is the marker. It tells the loader this is a plugin, and where to find the entry point.291**`openassistant.json`** (optional, for config validation):
59
60- `entry` defaults to `./index.ts` (then `./index.js`, `./src/index.ts`).
61- Plugin-specific dependencies go in `dependencies`. `@openassistant/sdk` is a `peerDependency` (the host provides it at load time).
62
63### `openassistant.json` (plugin manifest)
64
65```json292```json
66{293{
67 "id": "posthog",294 "id": "posthog",
68 "name": "PostHog Analytics",295 "name": "PostHog Analytics",
69 "description": "Read analytics and create monitors via PostHog API.",296 "description": "Read analytics and create monitors via PostHog API.",
70 "version": "0.1.0",
71 "configSchema": {297 "configSchema": {
72 "type": "object",298 "type": "object",
73 "required": ["apiKey", "projectId"],299 "required": ["apiKey", "projectId"],
@@ -81,457 +307,545 @@ The `"openassistant"` key is the marker. It tells the loader this is a plugin, a · 7 unmodified lines
81}307}
82```308```
83 309
84- `id` is the unique namespace. Tool paths become `posthog.analytics.getVisitors` etc.310**`index.ts`** (plugin entry):
85- `configSchema` is JSON Schema. Validated at load time against the user's config.
86- If no `openassistant.json` exists, the loader reads `id` from `package.json` name (strip scope) and config schema is treated as empty (no config required).
87
88### Entry Module
89
90Two supported patterns:
91
92**Pattern A: Export a ToolTree directly (simple case)**
93
94```ts
95import { defineTool } from "@openassistant/sdk";
96import { Effect } from "effect";
97
98export default {
99 analytics: {
100 getVisitors: defineTool({
101 id: "posthog.analytics.getVisitors",
102 kind: "read",
103 approval: "auto",
104 run: (input: { projectId: string }) =>
105 Effect.tryPromise(() => fetch(`...`).then(r => r.json())),
106 }),
107 },
108} satisfies ToolTree;
109```
110
111**Pattern B: Export a register function (needs config/context)**
112
113```ts311```ts
114import { defineTool, type PluginContext, type ToolTree } from "@openassistant/sdk";312import { defineTool, z, type PluginContext, type ToolTree } from "@openassistant/sdk";
115import { Effect } from "effect";313import { PostHogClient } from "posthog-node";
116 314
117export default function register(ctx: PluginContext): ToolTree {315export default function register(ctx: PluginContext): ToolTree {
118 const client = new PostHogClient(ctx.config.apiKey, ctx.config.host);316 const client = new PostHogClient(ctx.config.apiKey as string);
119 317
120 return {318 return {
121 analytics: {319 analytics: {
122 getVisitors: defineTool({320 getVisitors: defineTool({
123 kind: "read",321 description: "Get visitor count for a website",
124 approval: "auto",322 approval: "auto",
125 run: (input: { website: string }) =>323 args: z.object({ website: z.string() }),
126 Effect.tryPromise(() => client.getInsight(input.website)),324 run: async (input) => client.getInsight(input.website),
127 }),325 }),
128 },326 },
129 monitor: {327 monitor: {
130 createThreshold: defineTool({328 createThreshold: defineTool({
131 kind: "write",329 description: "Create a threshold alert for visitor count",
132 approval: "required",330 approval: "required",
133 run: (input: { website: string; threshold: number }) =>331 args: z.object({ website: z.string(), threshold: z.number() }),
134 Effect.tryPromise(() => client.createAlert(input)),332 run: async (input) => client.createAlert(input),
135 }),333 }),
136 },334 },
137 };335 };
138}336}
139```337```
140 338
141`PluginContext` provides:339Same `defineTool()`, same API. The only difference is the plugin exports a function that receives config via `PluginContext`, instead of exporting tools directly.
142- `config`: validated plugin config from the user's config file.
143- `logger`: scoped logger.
144- `dataDir`: per-plugin persistent data directory (`~/.config/openassistant/data/<pluginId>/`).
145 340
146---341---
147 342
148## Config File343## Config File
149 344
150Location: `~/.config/openassistant/config.json` (or `OPENASSISTANT_CONFIG_PATH` env var).345### Location Resolution
151 346
152For monorepo users: `.openassistant/config.json` in the project root (detected by walking up from cwd).347Config is found by scanning these locations (higher priority wins):
153 348
154Resolution order:349| Priority | Location | Env Override |
1551. Project-local: `<project>/.openassistant/config.json`350|----------|----------|--------------|
1562. Global: `~/.config/openassistant/config.json`351| 1 (highest) | `OPENASSISTANT_CONFIG` env var (path to file) | — |
1573. Merged (project overrides global).352| 2 | `<project>/.openassistant/config.json` | — |
353| 3 (lowest) | `~/.config/openassistant/config.json` | `OPENASSISTANT_CONFIG_DIR` |
158 354
159```json355Project root is detected by walking up from cwd looking for `.openassistant/` directory or `package.json`.
356
357When both project and global exist, they're deep-merged (project overrides global).
358
359### Config Shape
360
361```jsonc
160{362{
161 "plugins": {363 // Plugin specifiers — npm packages or file:// URLs
162 "paths": [364 "plugins": [
163 "./tools/posthog",365 "@openassistant/plugin-posthog",
164 "./tools/github",366 "@openassistant/plugin-github@^0.2.0",
165 "/absolute/path/to/custom-plugin"367 "file:///absolute/path/to/plugin",
166 ],368 "./relative/path/to/plugin"
167 "npm": [369 ],
168 "@openassistant/plugin-github",370
169 "@openassistant/plugin-posthog@^0.2.0"371 // Per-plugin configuration
170 ],372 "config": {
171 "config": {373 "posthog": {
172 "posthog": {374 "enabled": true,
173 "enabled": true,375 "apiKey": "${POSTHOG_API_KEY}",
174 "apiKey": "${POSTHOG_API_KEY}",376 "projectId": "12345",
175 "projectId": "12345",377 "host": "https://us.posthog.com"
176 "host": "https://us.posthog.com"378 },
177 },379 "github": {
178 "github": {380 "enabled": true,
179 "enabled": true,381 "token": "${GITHUB_TOKEN}"
180 "token": "${GITHUB_TOKEN}"
181 }
182 }382 }
183 },383 },
384
385 // Auth profiles
184 "auth": {386 "auth": {
185 "anthropic": {387 "anthropic": {
186 "type": "token",388 "type": "token",
187 "profileOrder": ["manual", "env"]389 "profileOrder": ["manual", "env"]
188 }390 }
391 },
392
393 // Tool-level overrides
394 "tools": {
395 "weather": true, // enable (default)
396 "dangerous-tool": false // disable
189 }397 }
190}398}
191```399```
192 400
193### Plugin Sources (Discovery Origins)401- **`plugins` is a flat array** (like OpenCode). The loader auto-detects whether a specifier is an npm package, a path, or a URL.
402- **`config` is top-level**, keyed by plugin ID.
403- **`tools` is a simple enable/disable map.** Individual tools can be toggled without touching plugin config.
404- **`${ENV_VAR}` interpolation** for secrets. Only in string values. No shell expansion.
194 405
195Plugins are discovered from these origins, in priority order:406---
196 407
197| Origin | Location | Use Case |408## Discovery and Loading
198|--------|----------|----------|
199| `config.plugins.paths` | Explicit paths from config | Monorepo workspace tools, local dev |
200| `workspace` | `<project>/.openassistant/plugins/` | Project-scoped plugins dropped into a convention dir |
201| `global` | `~/.config/openassistant/plugins/` | User-global plugins (installed via CLI) |
202| `npm` | `config.plugins.npm` entries resolved from `node_modules` | npm-installed plugins |
203| `bundled` | `<openassistant-package>/plugins/` | Shipped with the core package |
204 409
205### Plugin Config410### Convention Directories
206 411
207Config values support `${ENV_VAR}` interpolation so secrets don't live in config files.412These directories are auto-scanned for tools and plugins:
208 413
209Per-plugin config is validated against the plugin's `configSchema` at load time. If validation fails, the plugin is skipped with a warning (not a crash).414| Directory | What's Scanned |
415|-----------|----------------|
416| `<project>/.openassistant/tools/` | Standalone `.ts`/`.js` files |
417| `<project>/.openassistant/plugins/` | Plugin directories with `package.json` |
418| `~/.config/openassistant/tools/` | Global standalone tools |
419| `~/.config/openassistant/plugins/` | Global plugin directories |
210 420
211`enabled: false` in a plugin's config block disables it without removing its config.421### Auto-Dependency Management
212 422
213---423When the `.openassistant/` directory contains tools or plugins, the loader:
424
4251. Checks for `package.json` in `.openassistant/`.
4262. If missing, creates one with `@openassistant/sdk` as a dependency.
4273. Runs `bun install` if `node_modules` doesn't exist or `@openassistant/sdk` is missing.
428
429Users can just drop a `.ts` file in `tools/` and it works — the SDK dependency is auto-managed.
430
431```json
432// Auto-generated .openassistant/package.json
433{
434 "private": true,
435 "type": "module",
436 "dependencies": {
437 "@openassistant/sdk": "^0.1.0"
438 }
439}
440```
214 441
215## Discovery and Loading Pipeline442### Full Pipeline
216 443
217```444```
2181. RESOLVE CONFIG4451. RESOLVE CONFIG
219 - Find config file (project-local, then global)446 - Find config file (env > project > global)
220 - Parse and merge447 - Parse JSONC with ${ENV_VAR} interpolation
221 448 - Deep merge if multiple sources
2222. DISCOVER PLUGINS449
223 - Scan each origin (paths, workspace, global, npm, bundled)4502. SCAN CONVENTION DIRECTORIES
224 - For each candidate directory:451 - .openassistant/tools/*.{ts,js} → standalone tool candidates
225 a. Read package.json, check for "openassistant" key452 - .openassistant/plugins/*/ → plugin candidates (must have package.json)
226 b. Read openassistant.json (or derive defaults from package.json)453 - Same for global ~/.config/openassistant/
227 c. Deduplicate by plugin ID (first origin wins)454
228 4553. RESOLVE PLUGIN SPECIFIERS
2293. FILTER456 - For each entry in config.plugins[]:
230 - Check enabled/disabled state from config457 a. npm package name → resolve from node_modules
231 - Skip plugins with missing required config458 b. file:// URL → resolve absolute path
232 459 c. Relative path → resolve from project root
2334. VALIDATE CONFIG460
234 - Run JSON Schema validation on each plugin's config block4614. AUTO-INSTALL DEPENDENCIES
235 - Log warnings for invalid configs, skip those plugins462 - Ensure .openassistant/package.json exists with SDK dep
236 463 - Run bun install if needed
2375. LOAD MODULE464
238 - Use jiti (or Bun native import) to load the TypeScript entry point4655. DEDUPLICATE
239 - Alias "@openassistant/sdk" to the host's SDK module466 - By plugin ID (from manifest) or tool filename
240 467 - First-found wins (project > global > npm > bundled)
2416. RESOLVE EXPORT468
242 - If default export is a function: call it with PluginContext -> get ToolTree4696. LOAD MODULES
243 - If default export is a ToolTree: use directly470 - Use dynamic import() (Bun handles .ts natively)
244 - Namespace the tree under the plugin ID471 - For standalone tools: import file, extract default + named exports
245 472 - For plugins: import entry, resolve function vs ToolTree export
2467. MERGE TOOL TREES473
247 - Combine all plugin ToolTrees into the master ToolTree4747. VALIDATE CONFIG
475 - If plugin has openassistant.json with configSchema:
476 validate config[pluginId] against JSON Schema
477 - Skip with warning on validation failure
478
4798. VALIDATE INPUTS (at tool definition time)
480 - Each tool has a zod args schema
481 - At invocation time, parse input through schema before calling run
482
4839. BUILD TOOL TREE
484 - Standalone tools: flat under their filename ID
485 - Plugin tools: namespaced under plugin ID
486 - Check tools config for enable/disable overrides
248 - Detect ID collisions (error)487 - Detect ID collisions (error)
249 488
2508. GENERATE TYPE DECLARATIONS48910. GENERATE TYPE DECLARATIONS
251 - Walk the merged ToolTree490 - Walk merged ToolTree
252 - Emit TypeScript ambient declarations for the codemode typechecker491 - Derive TypeScript types from zod args schemas
253 - This replaces the hardcoded TOOL_DECLARATIONS string492 - This replaces the hardcoded TOOL_DECLARATIONS
254 493
2559. READY49411. READY
256 - Pass merged ToolTree to createCodeModeRunner()495 - Pass merged ToolTree to createCodeModeRunner()
257 - Plugin tools are now available in the sandbox496 - Pass declarations to typechecker
258```497```
259 498
260---499---
261 500
262## The SDK Package (`@openassistant/sdk`)501## The SDK Package (`@openassistant/sdk`)
263 502
264New package: `packages/sdk/`. This is the public API surface for plugin authors.503New package: `packages/sdk/`. This is the **only** package tool authors need. No Effect, no core internals.
265 504
266Exports:505### Exports
267- `defineTool()` (re-exported from core)
268- `ToolTree`, `ToolDefinition`, `ToolKind`, `ToolApprovalMode` types
269- `PluginContext` type
270- `z` (re-export zod for schema definitions)
271- `Effect` (re-export for handler authoring)
272 506
273```ts507```ts
274// packages/sdk/src/index.ts508// packages/sdk/src/index.ts
275export { defineTool, type ToolTree, type ToolDefinition, type ToolKind, type ToolApprovalMode } from "@openassistant/core";509export { defineTool, type ToolDefinition, type ToolTree } from "./define-tool.js";
276export type { PluginContext } from "./plugin-context.js";510export type { PluginContext } from "./plugin-context.js";
277export { z } from "zod";511export { z } from "zod";
278export { Effect } from "effect";
279```512```
280 513
281Plugin authors install `@openassistant/sdk` as a `peerDependency`. The host resolves it via aliasing at load time (same pattern as OpenClaw's jiti alias for `openclaw/plugin-sdk`).514That's it. Three things: `defineTool`, `PluginContext`, `z`.
515
516### What the SDK does NOT export
517
518- `Effect` — internal to core runtime
519- `ToolCallReceipt` — internal to core runtime
520- `ApprovalRequest` — internal to core runtime
521- `createCodeModeRunner` — internal to core runtime
522
523---
524
525## Changes to Core
526
527### `ToolDefinition` (the shape)
528
529The `ToolDefinition` interface changes from Effect-based to plain async, and drops `kind`:
530
531```ts
532// packages/core — BEFORE
533export interface ToolDefinition<TInput = unknown, TOutput = unknown> {
534 readonly _tag: "ToolDefinition";
535 readonly kind: ToolKind; // "read" | "write"
536 readonly approval: ToolApprovalMode; // "auto" | "required"
537 readonly run: (input: TInput) => Effect.Effect<TOutput, unknown>;
538 readonly previewInput?: (input: TInput) => string | undefined;
539 readonly previewOutput?: (output: TOutput) => string | undefined;
540}
541
542// packages/core — AFTER
543export interface ToolDefinition {
544 readonly _tag: "ToolDefinition";
545 readonly description: string;
546 readonly approval: "auto" | "required";
547 readonly args: z.ZodObject<any>;
548 readonly run: (input: unknown) => Promise<unknown>;
549 readonly previewInput?: (input: any) => string | undefined;
550 readonly previewOutput?: (output: unknown) => string | undefined;
551}
552```
553
554Changes:
5551. `run` returns `Promise<unknown>` instead of `Effect.Effect<TOutput, unknown>`
5562. `kind` removed — `approval` is the only policy field
5573. `args` (zod schema) added — required
5584. `description` added — required
5595. Generic type parameters removed (zod handles type safety at the authoring site, core treats everything as `unknown` internally)
560
561### `createToolInvoker()` (the runtime)
562
563The invoker validates input via zod, then wraps the plain async `run` in Effect:
564
565```ts
566// BEFORE: runs Effect directly, no input validation
567const execution = await Effect.runPromiseExit(params.definition.run(input));
568
569// AFTER: validates with zod, wraps async function in Effect
570const validated = params.definition.args.safeParse(input);
571if (!validated.success) {
572 // emit failed receipt with validation error, throw
573}
574const execution = await Effect.runPromiseExit(
575 Effect.tryPromise(() => params.definition.run(validated.data))
576);
577```
578
579### `createCodeModeRunner()` (unchanged API)
580
581The runner still takes a `ToolTree` and a `requestApproval` callback. Still returns an Effect. The public contract of the runner doesn't change — only the internal `ToolDefinition` shape does.
582
583### Migration of existing tools
584
585`apps/discord-bot/src/tools.ts` drops Effect imports and uses the new API:
586
587```ts
588// BEFORE
589import { defineTool } from "@openassistant/core";
590import { Effect } from "effect";
591
592calendar: {
593 update: defineTool({
594 kind: "write",
595 approval: "required",
596 run: (input: { title: string; startsAt: string }) =>
597 Effect.sync(() => calendarStore.update(input)),
598 }),
599}
600
601// AFTER
602import { defineTool, z } from "@openassistant/sdk";
603
604calendar: {
605 update: defineTool({
606 description: "Create or update a calendar event",
607 approval: "required",
608 args: z.object({
609 title: z.string(),
610 startsAt: z.string(),
611 notes: z.string().optional(),
612 }),
613 run: async (input) => calendarStore.update(input),
614 }),
615}
616```
617
618### Migration of `ToolCallReceipt`
619
620The `kind` field is also removed from `ToolCallReceipt`. Receipts track `approval` mode and `decision` — that's sufficient. The receipt shape becomes:
621
622```ts
623export interface ToolCallReceipt {
624 callId: string;
625 toolPath: string;
626 approval: "auto" | "required";
627 decision: "auto" | "approved" | "denied";
628 status: "succeeded" | "failed" | "denied";
629 timestamp: string;
630 inputPreview?: string;
631 outputPreview?: string;
632 error?: string;
633}
634```
282 635
283---636---
284 637
285## Type Declaration Generation638## Type Declaration Generation
286 639
287The current `code-typecheck.ts` has a hardcoded `TOOL_DECLARATIONS` string. This needs to be generated dynamically from the merged ToolTree.640### The Problem
288 641
289Approach:642`code-typecheck.ts` currently has a hardcoded `TOOL_DECLARATIONS` string. When tools are dynamically loaded, we need to generate this.
290- After loading all plugins and merging the ToolTree, walk the tree.
291- For each `ToolDefinition`, inspect the `run` function's parameter type (via the zod schema on the definition, which we add in the next iteration).
292- Generate ambient declarations: `declare const tools: { calendar: { update(input: { title: string; startsAt: string; notes?: string }): Promise<unknown>; }; };`
293- Pass this to the typechecker instead of the hardcoded string.
294 643
295**MVP shortcut**: For MVP, require plugins to export a `declarations` string alongside their ToolTree. Long-term, derive from schemas.644### Approach: Derive from Zod Schemas
645
646Every tool now has a zod `args` schema. We walk the merged ToolTree, convert each schema to a TypeScript type string, and emit ambient declarations.
296 647
297```ts648```ts
298// Plugin can optionally export declarations649// Given:
299export const declarations = `{650defineTool({
300 analytics: {651 description: "Get weather",
301 getVisitors(input: { projectId: string; website: string }): Promise<unknown>;652 approval: "auto",
302 };653 args: z.object({
303 monitor: {654 location: z.string(),
304 createThreshold(input: { website: string; threshold: number }): Promise<unknown>;655 units: z.enum(["metric", "imperial"]).default("metric"),
305 };656 }),
306}`;657 run: async (args) => { ... },
658});
659
660// Generates:
661// weather(input: { location: string; units?: "metric" | "imperial" }): Promise<unknown>
307```662```
308 663
309The loader concatenates all plugin declaration fragments into a single `declare const tools: { ... }` block.664For the full merged tree:
665```ts
666declare const tools: {
667 weather(input: { location: string; units?: "metric" | "imperial" }): Promise<unknown>;
668 posthog: {
669 analytics: {
670 getVisitors(input: { website: string }): Promise<unknown>;
671 };
672 monitor: {
673 createThreshold(input: { website: string; threshold: number }): Promise<unknown>;
674 };
675 };
676};
677```
310 678
311---679---
312 680
313## Monorepo User Experience681## Monorepo User Experience
314 682
315### Scenario: User has a Bun monorepo683### Scenario 1: Quick tool (30 seconds to working)
316 684
317```685```bash
318my-project/686mkdir -p .openassistant/tools
319 .openassistant/
320 config.json
321 plugins/ # Convention dir for local plugins
322 posthog/
323 package.json
324 openassistant.json
325 index.ts
326 apps/
327 my-app/
328 packages/
329 my-lib/
330```687```
331 688
332`.openassistant/config.json`:689Create `.openassistant/tools/weather.ts`:
333```json690```ts
334{691import { defineTool, z } from "@openassistant/sdk";
335 "plugins": {692
336 "config": {693export default defineTool({
337 "posthog": {694 description: "Get current weather",
338 "apiKey": "${POSTHOG_API_KEY}",695 approval: "auto",
339 "projectId": "12345"696 args: z.object({ location: z.string() }),
340 }697 run: async ({ location }) => {
341 }698 const res = await fetch(`https://wttr.in/${location}?format=j1`);
342 }699 return res.json();
343}700 },
701});
344```702```
345 703
346That's it. The convention directory is auto-scanned. Config is in one place.704Done. OpenAssistant auto-creates `package.json`, installs SDK, discovers the tool.
347 705
348### Scenario: User has tools as a workspace package706### Scenario 2: Workspace package plugin
349 707
350```708```
351my-project/709my-project/
352 .openassistant/710 .openassistant/
353 config.json711 config.json
354 packages/712 packages/
355 oa-tools/ # Workspace package with tools713 oa-posthog/
356 package.json # has "openassistant" key714 package.json # has "openassistant" key
357 openassistant.json715 openassistant.json # config schema
358 src/716 index.ts
359 index.ts
360```717```
361 718
362`.openassistant/config.json`:719`.openassistant/config.json`:
363```json720```jsonc
364{721{
365 "plugins": {722 "plugins": ["./packages/oa-posthog"],
366 "paths": ["./packages/oa-tools"]723 "config": {
724 "posthog": {
725 "apiKey": "${POSTHOG_API_KEY}",
726 "projectId": "12345"
727 }
367 }728 }
368}729}
369```730```
370 731
371Explicit path reference. Clean, no magic.732### Scenario 3: npm package
372
373### Scenario: User installs from npm
374 733
375```bash734```bash
376bun add @openassistant/plugin-github735bun add @openassistant/plugin-github
377```736```
378 737
379`.openassistant/config.json`:738`.openassistant/config.json`:
380```json739```jsonc
381{740{
382 "plugins": {741 "plugins": ["@openassistant/plugin-github"],
383 "npm": ["@openassistant/plugin-github"],742 "config": {
384 "config": {743 "github": { "token": "${GITHUB_TOKEN}" }
385 "github": {
386 "token": "${GITHUB_TOKEN}"
387 }
388 }
389 }744 }
390}745}
391```746```
392 747
393The loader resolves `@openassistant/plugin-github` from `node_modules`.748### Scenario 4: Multiple tools in one file
394
395---
396
397## CLI Surface (Future)
398
399```bash
400# Initialize config in current project
401openassistant init
402 749
403# List discovered plugins and their status750`.openassistant/tools/github-issues.ts`:
404openassistant plugins list751```ts
405 752import { defineTool, z } from "@openassistant/sdk";
406# Validate plugin configs
407openassistant plugins check
408
409# Install a plugin from npm to global plugins dir
410openassistant plugins install @openassistant/plugin-github
411 753
412# Scaffold a new plugin754export const list = defineTool({
413openassistant plugins create my-plugin755 description: "List GitHub issues",
756 approval: "auto",
757 args: z.object({
758 repo: z.string(),
759 state: z.enum(["open", "closed"]).default("open"),
760 }),
761 run: async ({ repo, state }) => { ... },
762});
763
764export const create = defineTool({
765 description: "Create a GitHub issue",
766 approval: "required",
767 args: z.object({
768 repo: z.string(),
769 title: z.string(),
770 body: z.string().optional(),
771 }),
772 run: async ({ repo, title, body }) => { ... },
773});
414```774```
415 775
416Not needed for MVP. The config file + discovery is enough.776Tools become: `tools.github_issues.list(...)` and `tools.github_issues.create(...)`.
417 777
418---778---
419 779
420## Implementation Plan780## Implementation Plan
421 781
422### Phase 1: Config + Discovery (Foundation)782### Phase 1: New `defineTool()` + SDK
423 783
4241. **Config loader** (`packages/core/src/config/`)7841. **Change `ToolDefinition` in core** — `run` becomes `Promise`, drop `kind`, add `args` + `description`
425 - Find config file (project-local walk-up, global fallback)7852. **Update `createToolInvoker()`** — wrap async `run` in Effect internally, add zod validation
426 - Parse JSON with `${ENV_VAR}` interpolation7863. **Update `ToolCallReceipt`** — drop `kind` field
427 - Type-safe config shape with validation7874. **Create `packages/sdk/`** — `defineTool()`, `z` re-export, `PluginContext` type
428 - Export `resolveConfig(): OpenAssistantConfig`7885. **Migrate `apps/discord-bot/src/tools.ts`** — use new API, drop Effect
7896. **Type declaration generator** — walk ToolTree, emit declarations from zod schemas
429 790
4302. **Plugin discovery** (`packages/core/src/plugins/discovery.ts`)791### Phase 2: Config + Discovery
431 - Scan each origin (paths, workspace dir, global dir)
432 - Read `package.json` "openassistant" key
433 - Read `openassistant.json` manifest
434 - Return `DiscoveredPlugin[]` with id, path, manifest, origin
435 792
4363. **Plugin loader** (`packages/core/src/plugins/loader.ts`)7936. **Config loader** (`packages/core/src/config/`)
437 - Load TypeScript entry via `jiti` or dynamic `import()`794 - Config file resolution (env > project > global)
438 - Resolve export pattern (function vs ToolTree)795 - JSONC parsing + `${ENV_VAR}` interpolation
439 - Validate plugin config against schema796 - Zod schema for config shape validation
440 - Return `LoadedPlugin { id, toolTree, declarations? }`
441 797
4424. **Plugin registry** (`packages/core/src/plugins/registry.ts`)7987. **Tool/Plugin discovery** (`packages/core/src/plugins/`)
443 - Merge all loaded plugin ToolTrees into master tree799 - Scan convention directories (`tools/`, `plugins/`)
444 - Namespace under plugin IDs800 - Resolve plugin specifiers (npm, file://, relative path)
445 - Detect collisions801 - Auto-dependency management for `.openassistant/`
446 - Export `createPluginRegistry(config): { toolTree: ToolTree, declarations: string }`802 - Deduplicate by ID
447 803
448### Phase 2: SDK Package8048. **Plugin loader** (`packages/core/src/plugins/loader.ts`)
449 805 - Dynamic `import()` for `.ts` files
4505. **Create `packages/sdk/`**806 - Resolve export pattern (function vs ToolTree vs individual tools)
451 - Barrel export of `defineTool`, types, `PluginContext`, `z`, `Effect`807 - Config validation against manifest schema
452 - `package.json` with proper `exports` map808 - Build merged ToolTree + declarations
453 - Will eventually be publishable to npm (remove `private: true`)
454 809
455### Phase 3: Wire Into Runtime810### Phase 3: Wire Into Runtime
456 811
4576. **Update `apps/discord-bot/`**8129. **Update `apps/discord-bot/`** — use plugin registry instead of hardcoded tools
458 - Replace hardcoded `createToolTree()` with `createPluginRegistry(config).toolTree`81310. **Update `apps/gateway/`** — same
459 - Replace hardcoded `TOOL_DECLARATIONS` with generated declarations
460 - Config-driven tool loading
461
4627. **Update `apps/gateway/`**
463 - Same plugin registry integration
464 814
465### Phase 4: Developer Experience815### Phase 4: DX Polish
466 816
4678. **`openassistant init` command** (scaffold `.openassistant/config.json`)81711. **`openassistant init`** — scaffold `.openassistant/config.json`
4689. **`openassistant plugins create` command** (scaffold plugin directory)81812. **`openassistant tools create <name>`** — scaffold a tool file
46910. **Documentation** for plugin authors81913. **`openassistant plugins list`** — show discovered tools/plugins and their status
470 820
471---821---
472 822
473## Key Decisions to Make823## Key Decisions (Resolved)
474 824
475### 1. Module Loading Strategy825| Decision | Choice | Rationale |
476 826|----------|--------|-----------|
477**Option A: jiti** (like OpenClaw)827| Tool authoring API | Plain async `run` function | Effect is a runtime internal. Tool authors shouldn't know it exists. |
478- Pros: Battle-tested, works in Node and Bun, handles TypeScript natively.828| Tool policy | `approval: "auto" \| "required"` only | No `kind`/`risk` classification. Tool author sets approval default. Operators can override via config. |
479- Cons: Another dependency, may have edge cases with Effect imports.829| Module loading | Bun native `import()` | We're Bun-only. Zero deps. Bun handles .ts natively. |
480 830| Config format | JSONC | Comments are essential for config files. |
481**Option B: Bun native `import()`**831| Config location | `.openassistant/config.json` | Follows OpenCode's `.opencode/` pattern. |
482- Pros: Zero deps, Bun handles TypeScript natively, fast.832| Plugin specifiers | Flat array (like OpenCode) | Simpler than `{ paths, npm }`. Auto-detect type from specifier. |
483- Cons: Only works in Bun runtime, may need `file://` URL construction, plugin resolution is less configurable.833| Tool schemas | Required zod `args` on every tool | Enables type declaration gen, validation, and docs from one source. |
484 834| Namespacing | Plugin ID from manifest or dirname | Plugin tools namespaced automatically. Standalone tools are flat. |
485**Option C: Bun `import()` with fallback to jiti for Node compat**835| Hot reload | No (MVP) | Bun `--hot` covers dev. True registry reload is Phase 2+. |
486- Pros: Best of both. Fast path for Bun, fallback for portability.836| SDK package | Separate (`packages/sdk/`) | Clean boundary. No Effect dependency for tool authors. |
487
488**Recommendation: Option B for MVP (Bun-only).** We're already Bun-native. Use dynamic `import()` with path resolution. Add jiti as a fallback later if Node support matters.
489
490### 2. Where Does Config Live?
491
492**Recommendation:**
493- Project-local: `.openassistant/config.json` (detected by walking up from cwd looking for `.openassistant/` or `package.json` with `"openassistant"` key).
494- Global: `~/.config/openassistant/config.json`.
495- Merge strategy: project-local overrides global (deep merge for `plugins.config`).
496
497### 3. Plugin ID Namespacing
498
499**Option A:** Plugin tools are auto-namespaced: `tools.<pluginId>.<toolName>`.
500**Option B:** Plugin chooses its own namespace via manifest `id`.
501**Option C:** Flat merge, plugin responsible for unique paths.
502
503**Recommendation: Option B.** The manifest `id` is the namespace. `tools.posthog.analytics.getVisitors`. The `id` is the top-level key in the merged tree.
504
505### 4. Schema on Tool Definitions
506
507Currently `defineTool()` doesn't take an input schema -- the `run` function signature is the contract. For type declaration generation, we need schemas.
508
509**Recommendation:** Add optional `input` schema (zod) to `defineTool()`. Not required for MVP (use exported `declarations` string as bridge), but strongly encouraged.
510
511```ts
512defineTool({
513 kind: "read",
514 approval: "auto",
515 input: z.object({ projectId: z.string() }), // NEW
516 run: (input) => Effect.tryPromise(() => ...),
517})
518```
519
520### 5. Hot Reload
521
522**MVP: No.** Restart to pick up plugin changes. Gateway already supports `--hot` via Bun, which covers dev workflow. True hot-reload of plugin registry is Phase 2+.
523 837
524---838---
525 839
526## Security Considerations840## Security Considerations
527 841
5281. **Plugins run in the same process.** No sandbox between plugins and the gateway. This is intentional for MVP -- the sandbox is between the LLM-generated code and the tool implementations, not between plugins.8421. **Plugins run in the same process.** No sandbox between plugins and the gateway. The sandbox boundary is between LLM-generated code and tool implementations, not between plugins.
529 843
5302. **Config interpolation** only supports `${ENV_VAR}` syntax. No shell expansion, no nested interpolation, no code execution.8442. **Config interpolation** only supports `${ENV_VAR}` syntax. No shell expansion, no nested interpolation, no code execution.
531 845
5323. **Plugin code is trusted.** If a user puts a plugin in their config, they trust it. We don't scan for malicious code (unlike OpenClaw's warning-only scan).8463. **Plugin code is trusted.** If a user puts a plugin in their config, they trust it. No malicious code scanning.
533 847
5344. **Secrets in config** should use env var interpolation. We should warn (not block) if literal API keys appear in config files.8484. **Secrets in config** should use env var interpolation. We log a warning (not block) if literal API keys appear in config files.
535 849
536---850---
537 851
@@ -543,36 +857,37 @@ packages/ · 5 unmodified lines
543 src/857 src/
544 index.ts858 index.ts
545 codemode/859 codemode/
546 runner.ts # Existing, unchanged860 runner.ts # UPDATED: ToolDefinition uses Promise, adds zod validation
861 runner.test.ts
547 config/862 config/
548 loader.ts # NEW: config file resolution + parsing863 loader.ts # NEW: config file resolution + parsing
549 types.ts # NEW: config schema types864 types.ts # NEW: config shape (Zod schema)
550 interpolation.ts # NEW: ${ENV_VAR} expansion865 interpolation.ts # NEW: ${ENV_VAR} expansion
551 plugins/866 plugins/
552 discovery.ts # NEW: scan origins for plugin candidates867 discovery.ts # NEW: scan convention dirs + resolve specifiers
553 loader.ts # NEW: load plugin TypeScript modules868 loader.ts # NEW: dynamic import + export resolution
554 manifest.ts # NEW: parse/validate openassistant.json869 manifest.ts # NEW: parse/validate openassistant.json
555 registry.ts # NEW: merge ToolTrees, generate declarations870 registry.ts # NEW: merge ToolTrees, deduplicate, enable/disable
556 types.ts # NEW: DiscoveredPlugin, LoadedPlugin, etc.871 declarations.ts # NEW: generate TypeScript declarations from zod schemas
872 auto-install.ts # NEW: auto-create package.json + bun install
873 types.ts # NEW: DiscoveredPlugin, LoadedPlugin, etc.
557 sdk/874 sdk/
558 package.json # NEW: @openassistant/sdk875 package.json # NEW: @openassistant/sdk
559 src/876 src/
560 index.ts # NEW: public API barrel877 index.ts # NEW: exports defineTool, z, PluginContext
561 plugin-context.ts # NEW: PluginContext type878 define-tool.ts # NEW: defineTool() — creates ToolDefinition, no Effect
879 plugin-context.ts # NEW: PluginContext type
562```880```
563 881
564---882---
565 883
566## Open Questions884## Open Questions
567 885
5681. **Should the SDK be a separate package or an export path from core?**8861. **Do we validate tool inputs at runtime?**
569 Leaning separate package (`packages/sdk/`) for clean dependency boundary. Plugin authors only need the SDK, not core internals.887 Yes. Parse input through zod schema before calling `run`. This catches bad LLM-generated inputs early with clear error messages in receipts.
570
5712. **How do we handle plugin dependencies at install time?**
572 For workspace/path plugins: they manage their own deps. For npm plugins: deps come with the package. For global install: we may need `bun install` in the plugin dir (like OpenClaw does `npm install --omit=dev`).
573 888
5743. **Do we need a plugin lockfile?**8892. **How do we handle tools that need persistent state?**
575 Probably not for MVP. Config file is declarative. `bun.lock` in the host project handles npm-sourced plugins.890 `PluginContext.dataDir` gives plugins a persistent directory. Not needed for standalone tools in MVP.
576 891
5774. **Should plugins be able to register things other than tools?**8923. **Should we support `openassistant.jsonc` (comments)?**
578 Not for MVP. But the `register(ctx)` function pattern leaves the door open for `ctx.registerHook()`, `ctx.registerCommand()`, etc. later.893 Yes. Both `config.json` and `config.jsonc` should be accepted.
apps/discord-bot/package.json+2−0
@@ -13,6 +13,8 @@ · 12 unmodified lines
13 },13 },
14 "dependencies": {14 "dependencies": {
15 "@ai-sdk/anthropic": "^2.0.0",15 "@ai-sdk/anthropic": "^2.0.0",
16 "@effect/platform": "latest",
17 "@effect/rpc": "latest",
16 "@openassistant/core": "workspace:*",18 "@openassistant/core": "workspace:*",
17 "@openassistant/reacord": "workspace:*",19 "@openassistant/reacord": "workspace:*",
18 "ai": "^6.0.73",20 "ai": "^6.0.73",
apps/discord-bot/src/agent-loop.ts+1−1
@@ -180,7 +180,7 @@ function buildAgentPrompt(userPrompt: string, now: Date): string { · 179 unmodified lines
180 "run_code expects JavaScript function-body code executed as new AsyncFunction('tools', code).",180 "run_code expects JavaScript function-body code executed as new AsyncFunction('tools', code).",
181 "Inside code, call available tools directly like: await tools.calendar.update({ title, startsAt, notes }).",181 "Inside code, call available tools directly like: await tools.calendar.update({ title, startsAt, notes }).",
182 "For multiple events, produce multiple tool calls in the same code block.",182 "For multiple events, produce multiple tool calls in the same code block.",
183 "Never claim an action succeeded unless run_code returned ok=true and receipts show succeeded calls.",183 "Never claim an action succeeded unless run_code returned ok=true.",
184 `Current timestamp: ${now.toISOString()}`,184 `Current timestamp: ${now.toISOString()}`,
185 `User request: ${userPrompt}`,185 `User request: ${userPrompt}`,
186 ].join("\n");186 ].join("\n");
apps/discord-bot/src/format-response.test.ts+9−82
@@ -1,96 +1,23 @@
1import type { CodeModeRunResult } from "@openassistant/core";
2import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
3import { formatDiscordResponse } from "./format-response.js";2import { formatDiscordResponse } from "./format-response.js";
4 3
5describe("formatDiscordResponse", () => {4describe("formatDiscordResponse", () => {
6 it("renders a polished verified calendar summary", () => {5 it("returns trimmed text and footer", () => {
7 const output = formatDiscordResponse({6 const output = formatDiscordResponse({
8 prompt: "Add 3 calendar events",7 text: " Added dinner to your calendar. ",
9 text: "Added three events.",8 footer: "planner details",
10 planner: "Claude tool-loop (3 code runs).",
11 provider: "claude",
12 runs: [
13 { code: "run1", result: runResult("a", "succeeded", "Dinner @ 2026-02-07T17:00:00.000Z") },
14 { code: "run2", result: runResult("b", "succeeded", "Lunch @ 2026-02-08T14:00:00.000Z") },
15 { code: "run3", result: runResult("c", "succeeded", "Breakfast @ 2026-02-09T03:00:00.000Z") },
16 ],
17 });9 });
18 10
19 expect(output.message).toContain("Added 3 calendar events.");11 expect(output.message).toBe("Added dinner to your calendar.");
20 expect(output.message).toContain("- Dinner -");12 expect(output.footer).toBe("planner details");
21 expect(output.message).toContain("- Lunch -");
22 expect(output.message).toContain("- Breakfast -");
23 expect(output.footer).toBeDefined();
24 expect(output.footer).toContain("Activity: 3 tool calls, 3 approved.");
25 });13 });
26 14
27 it("shows concise recovery message when some attempts fail but success exists", () => {15 it("falls back to Done when text is empty", () => {
28 const output = formatDiscordResponse({16 const output = formatDiscordResponse({
29 prompt: "Delete event",17 text: " ",
30 text: "Could not complete request.",
31 planner: "Claude tool-loop (2 code runs).",
32 provider: "claude",
33 runs: [
34 {
35 code: "bad",
36 result: {
37 ok: false,
38 error: "Typecheck failed: Property 'create' does not exist",
39 receipts: [],
40 },
41 },
42 { code: "good", result: runResult("ok", "succeeded", "Dinner @ 2026-02-07T17:00:00.000Z") },
43 ],
44 });18 });
45 19
46 expect(output.message).toContain("Added 1 calendar event.");20 expect(output.message).toBe("Done.");
47 expect(output.message).toContain("Recovered from 1 issue during execution.");21 expect(output.footer).toBeUndefined();
48 expect(output.message).not.toContain("code run failed");
49 });
50
51 it("shows actionable error message when nothing succeeded", () => {
52 const output = formatDiscordResponse({
53 prompt: "Delete event",
54 text: "Could not complete request.",
55 planner: "Claude tool-loop (1 code run).",
56 provider: "claude",
57 runs: [
58 {
59 code: "run",
60 result: {
61 ok: false,
62 error: "Typecheck failed: Property 'create' does not exist",
63 receipts: [],
64 },
65 },
66 ],
67 });
68
69 expect(output.message).toContain("I couldn't complete that request.");
70 expect(output.message).toContain("I hit an issue:");
71 expect(output.message).toContain("Typecheck failed");
72 });22 });
73});23});
74
75function runResult(
76 callId: string,
77 status: "succeeded" | "failed",
78 inputPreview: string,
79): CodeModeRunResult {
80 return {
81 ok: true,
82 value: { ok: true },
83 receipts: [
84 {
85 callId,
86 toolPath: "calendar.update",
87 kind: "write",
88 approval: "required",
89 decision: "approved",
90 status,
91 timestamp: "2026-02-06T01:00:00.000Z",
92 inputPreview,
93 },
94 ],
95 };
96}
apps/discord-bot/src/format-response.ts+4−259
@@ -1,275 +1,20 @@
1import type { ToolCallReceipt } from "@openassistant/core";
2import type { AgentCodeRun } from "./agent-loop.js";
3
4export interface FormattedDiscordResponse {1export interface FormattedDiscordResponse {
5 message: string;2 message: string;
6 footer?: string;3 footer?: string;
7}4}
8 5
9export function formatDiscordResponse(params: {6export function formatDiscordResponse(params: {
10 prompt: string;
11 text: string;7 text: string;
12 planner: string;8 footer?: string | undefined;
13 provider: "claude";
14 runs: AgentCodeRun[];
15}): FormattedDiscordResponse {9}): FormattedDiscordResponse {
16 const { text, planner, provider, runs } = params;10 const message = params.text.trim().length > 0 ? params.text.trim() : "Done.";
17 const receipts = runs.flatMap((run) => run.result.receipts);
18 const stats = summarizeReceipts(receipts);
19 const failures = summarizeFailures(runs);
20
21 const messageSections: string[] = [];
22 messageSections.push(buildPrimaryReply(text, receipts, stats));
23
24 const issueSummary = buildIssueSummary(stats, failures);
25 if (issueSummary) {
26 messageSections.push(issueSummary);
27 }
28
29 const footerParts: string[] = [];
30 const toolFooter = buildToolFooter(receipts, stats);
31 if (toolFooter) {
32 footerParts.push(toolFooter);
33 }
34
35 if (isVerboseMode()) {
36 footerParts.push(`debug: ${provider} - ${planner}`);
37 footerParts.push(`runs: ${runs.length}`);
38 footerParts.push(`tools: ${formatToolStats(stats.byTool)}`);
39 footerParts.push(
40 failures.length > 0
41 ? `failures: ${failures.map((failure) => failure.message).join(" | ")}`
42 : "failures: none",
43 );
44 }
45 11
46 return {12 return {
47 message: truncateDiscord(messageSections.filter((value) => value.trim().length > 0).join("\n\n")),13 message: truncateDiscord(message),
48 ...(footerParts.length > 0 ? { footer: truncateFooter(footerParts.join(" | ")) } : {}),14 ...(params.footer ? { footer: truncateFooter(params.footer) } : {}),
49 };15 };
50}16}
51 17
52type ToolStats = {
53 calls: number;
54 approved: number;
55 auto: number;
56 denied: number;
57 succeeded: number;
58 failed: number;
59};
60
61type ReceiptSummary = {
62 totals: ToolStats;
63 byTool: Map<string, ToolStats>;
64};
65
66type FailureEntry = {
67 message: string;
68};
69
70function summarizeReceipts(receipts: ToolCallReceipt[]): ReceiptSummary {
71 const totals: ToolStats = {
72 calls: 0,
73 approved: 0,
74 auto: 0,
75 denied: 0,
76 succeeded: 0,
77 failed: 0,
78 };
79
80 const byTool = new Map<string, ToolStats>();
81
82 for (const receipt of receipts) {
83 totals.calls += 1;
84 if (receipt.decision === "approved") {
85 totals.approved += 1;
86 } else if (receipt.decision === "auto") {
87 totals.auto += 1;
88 } else {
89 totals.denied += 1;
90 }
91
92 if (receipt.status === "succeeded") {
93 totals.succeeded += 1;
94 } else if (receipt.status === "failed") {
95 totals.failed += 1;
96 } else {
97 totals.denied += 1;
98 }
99
100 const current = byTool.get(receipt.toolPath) ?? {
101 calls: 0,
102 approved: 0,
103 auto: 0,
104 denied: 0,
105 succeeded: 0,
106 failed: 0,
107 };
108
109 current.calls += 1;
110 if (receipt.decision === "approved") {
111 current.approved += 1;
112 } else if (receipt.decision === "auto") {
113 current.auto += 1;
114 } else {
115 current.denied += 1;
116 }
117
118 if (receipt.status === "succeeded") {
119 current.succeeded += 1;
120 } else if (receipt.status === "failed") {
121 current.failed += 1;
122 } else {
123 current.denied += 1;
124 }
125
126 byTool.set(receipt.toolPath, current);
127 }
128
129 return { totals, byTool };
130}
131
132function summarizeFailures(runs: AgentCodeRun[]): FailureEntry[] {
133 const failures: FailureEntry[] = [];
134 for (const run of runs) {
135 if (!run.result.ok) {
136 failures.push({ message: run.result.error });
137 }
138
139 for (const receipt of run.result.receipts) {
140 if (receipt.status === "failed") {
141 failures.push({
142 message: `${receipt.toolPath} failed${receipt.error ? `: ${receipt.error}` : ""}`,
143 });
144 }
145 if (receipt.status === "denied") {
146 failures.push({
147 message: `${receipt.toolPath} was denied`,
148 });
149 }
150 }
151 }
152 return failures;
153}
154
155function buildPrimaryReply(text: string, receipts: ToolCallReceipt[], stats: ReceiptSummary): string {
156 const calendarUpdates = extractCalendarUpdates(receipts);
157 if (calendarUpdates.length > 0) {
158 const header =
159 calendarUpdates.length === 1
160 ? "Added 1 calendar event."
161 : `Added ${calendarUpdates.length} calendar events.`;
162 const lines = [header, ...calendarUpdates.map((entry) => `- ${entry.title} - ${formatIsoUtc(entry.startsAt)}`)];
163 return lines.join("\n");
164 }
165
166 if (stats.totals.succeeded > 0) {
167 return text.trim() || "Completed.";
168 }
169
170 return "I couldn't complete that request.";
171}
172
173function buildToolFooter(receipts: ToolCallReceipt[], stats: ReceiptSummary): string | null {
174 if (receipts.length === 0) {
175 return null;
176 }
177
178 const parts: string[] = [];
179 parts.push(`${stats.totals.calls} tool call${stats.totals.calls === 1 ? "" : "s"}`);
180 if (stats.totals.approved > 0) {
181 parts.push(`${stats.totals.approved} approved`);
182 }
183 if (stats.totals.auto > 0) {
184 parts.push(`${stats.totals.auto} auto`);
185 }
186 if (stats.totals.failed > 0) {
187 parts.push(`${stats.totals.failed} failed`);
188 }
189 if (stats.totals.denied > 0) {
190 parts.push(`${stats.totals.denied} denied`);
191 }
192
193 return `Activity: ${parts.join(", ")}.`;
194}
195
196function buildIssueSummary(stats: ReceiptSummary, failures: FailureEntry[]): string | null {
197 if (failures.length === 0) {
198 return null;
199 }
200
201 if (stats.totals.succeeded > 0) {
202 return `Recovered from ${failures.length} issue${failures.length === 1 ? "" : "s"} during execution.`;
203 }
204
205 const top = failures.slice(0, 2).map((failure) => `- ${failure.message}`);
206 return ["I hit an issue:", ...top].join("\n");
207}
208
209function extractCalendarUpdates(receipts: ToolCallReceipt[]): Array<{ title: string; startsAt: string }> {
210 const updates: Array<{ title: string; startsAt: string }> = [];
211
212 for (const receipt of receipts) {
213 if (receipt.toolPath !== "calendar.update" || receipt.status !== "succeeded") {
214 continue;
215 }
216 const preview = receipt.inputPreview;
217 if (!preview) {
218 continue;
219 }
220 const split = preview.lastIndexOf(" @ ");
221 if (split <= 0 || split >= preview.length - 3) {
222 continue;
223 }
224
225 const title = preview.slice(0, split).trim();
226 const startsAt = preview.slice(split + 3).trim();
227 if (!title || !startsAt) {
228 continue;
229 }
230
231 updates.push({ title, startsAt });
232 }
233
234 return updates;
235}
236
237function formatIsoUtc(value: string): string {
238 const date = new Date(value);
239 if (Number.isNaN(date.getTime())) {
240 return value;
241 }
242 return new Intl.DateTimeFormat("en-US", {
243 timeZone: "UTC",
244 weekday: "short",
245 month: "short",
246 day: "numeric",
247 year: "numeric",
248 hour: "numeric",
249 minute: "2-digit",
250 hour12: true,
251 }).format(date) + " UTC";
252}
253
254function formatToolStats(byTool: Map<string, ToolStats>): string {
255 if (byTool.size === 0) {
256 return "none";
257 }
258 return Array.from(byTool.entries())
259 .map(([tool, stat]) => `${tool}: ${stat.calls} calls, ${stat.succeeded} succeeded${stat.failed ? `, ${stat.failed} failed` : ""}${stat.denied ? `, ${stat.denied} denied` : ""}`)
260 .join("; ");
261}
262
263function isVerboseMode(): boolean {
264 const value = readEnv("OPENASSISTANT_VERBOSE_RESPONSE")?.trim().toLowerCase();
265 return value === "1" || value === "true" || value === "yes";
266}
267
268function readEnv(key: string): string | undefined {
269 const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun;
270 return bun?.env?.[key] ?? process.env[key];
271}
272
273function truncateDiscord(value: string): string {18function truncateDiscord(value: string): string {
274 const limit = 1_900;19 const limit = 1_900;
275 if (value.length <= limit) {20 if (value.length <= limit) {
apps/discord-bot/src/gateway-client.ts+35−0
@@ -0,0 +1,35 @@
1import { FetchHttpClient } from "@effect/platform";
2import { RpcClient, RpcSerialization } from "@effect/rpc";
3import { AgentRpcs, type RunTurnOutput } from "@openassistant/core";
4import { Effect, Layer } from "effect";
5
6const gatewayUrl = readEnv("OPENASSISTANT_GATEWAY_URL") ?? "http://127.0.0.1:8787/rpc";
7
8const GatewayRpcClientLive = RpcClient.layerProtocolHttp({
9 url: gatewayUrl,
10}).pipe(Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]));
11
12export interface RunGatewayTurnInput {
13 prompt: string;
14 requesterId: string;
15 channelId: string;
16}
17
18export async function runGatewayTurn(input: RunGatewayTurnInput): Promise<RunTurnOutput> {
19 return Effect.runPromise(
20 Effect.gen(function* () {
21 const client = yield* RpcClient.make(AgentRpcs);
22 return yield* client.RunTurn({
23 prompt: input.prompt,
24 requesterId: input.requesterId,
25 channelId: input.channelId,
26 nowIso: new Date().toISOString(),
27 });
28 }).pipe(Effect.scoped, Effect.provide(GatewayRpcClientLive)),
29 );
30}
31
32function readEnv(key: string): string | undefined {
33 const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun;
34 return bun?.env?.[key] ?? process.env[key];
35}
apps/discord-bot/src/index.tsx+7−30
@@ -1,4 +1,3 @@
1import { createCodeModeRunner } from "@openassistant/core";
2import { makeReacord } from "@openassistant/reacord";1import { makeReacord } from "@openassistant/reacord";
3import {2import {
4 ChannelType,3 ChannelType,
@@ -10,20 +9,15 @@ import { · 5 unmodified lines
10 type SendableChannels,9 type SendableChannels,
11} from "discord.js";10} from "discord.js";
12import { Effect } from "effect";11import { Effect } from "effect";
13import { DiscordApprovalBridge } from "./approval-bridge.js";
14import { runAgentLoop } from "./agent-loop.js";
15import { InMemoryCalendarStore } from "./calendar-store.js";
16import { AssistantReplyView, AssistantWorkingView } from "./discord-views.js";12import { AssistantReplyView, AssistantWorkingView } from "./discord-views.js";
17import { formatDiscordResponse } from "./format-response.js";13import { formatDiscordResponse } from "./format-response.js";
18import { createToolTree } from "./tools.js";14import { runGatewayTurn } from "./gateway-client.js";
19 15
20const token = Bun.env.DISCORD_BOT_TOKEN;16const token = Bun.env.DISCORD_BOT_TOKEN;
21if (!token) {17if (!token) {
22 throw new Error("Missing DISCORD_BOT_TOKEN");18 throw new Error("Missing DISCORD_BOT_TOKEN");
23}19}
24 20
25const approvalTimeoutMs = Number(Bun.env.OPENASSISTANT_APPROVAL_TIMEOUT_MS ?? 300_000);
26
27const client = new Client({21const client = new Client({
28 intents: [22 intents: [
29 GatewayIntentBits.Guilds,23 GatewayIntentBits.Guilds,
@@ -34,10 +28,7 @@ const client = new Client({ · 4 unmodified lines
34 partials: [Partials.Channel],28 partials: [Partials.Channel],
35});29});
36 30
37const approvalBridge = new DiscordApprovalBridge(client, approvalTimeoutMs);
38const reacord = makeReacord(client);31const reacord = makeReacord(client);
39const calendarStore = new InMemoryCalendarStore();
40const tools = createToolTree(calendarStore);
41 32
42client.once(Events.ClientReady, (readyClient) => {33client.once(Events.ClientReady, (readyClient) => {
43 console.log(`[discord-bot] logged in as ${readyClient.user.tag}`);34 console.log(`[discord-bot] logged in as ${readyClient.user.tag}`);
@@ -70,29 +61,15 @@ async function handleMessage(message: Message): Promise<void> { · 26 unmodified lines
70 );61 );
71 62
72 try {63 try {
73 const runner = createCodeModeRunner({64 const generated = await runGatewayTurn({
74 tools,65 prompt: message.content,
75 requestApproval: (request) =>66 requesterId: message.author.id,
76 Effect.tryPromise({67 channelId: message.channelId,
77 try: () =>
78 approvalBridge.requestApproval({
79 request,
80 channel: approvalChannel,
81 requesterId: message.author.id,
82 }),
83 catch: (error) => error,
84 }),
85 });68 });
86 69
87 const generated = await runAgentLoop(message.content, (code) =>
88 Effect.runPromise(runner.run({ code })),
89 );
90 const response = formatDiscordResponse({70 const response = formatDiscordResponse({
91 prompt: message.content,71 text: generated.message,
92 text: generated.text,72 footer: generated.footer,
93 planner: generated.planner,
94 provider: generated.provider,
95 runs: generated.runs,
96 });73 });
97 74
98 instance.render(<AssistantReplyView message={response.message} footer={response.footer} />);75 instance.render(<AssistantReplyView message={response.message} footer={response.footer} />);
apps/gateway/package.json+7−1
@@ -15,7 +15,13 @@ · 14 unmodified lines
15 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""15 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""
16 },16 },
17 "dependencies": {17 "dependencies": {
18 "@ai-sdk/anthropic": "^2.0.0",
19 "@effect/platform": "latest",
20 "@effect/platform-bun": "latest",
21 "@effect/rpc": "latest",
18 "@openassistant/core": "workspace:*",22 "@openassistant/core": "workspace:*",
19 "effect": "latest"23 "ai": "^6.0.73",
24 "effect": "latest",
25 "zod": "^4.3.6"
20 }26 }
21}27}
apps/gateway/src/agent-loop.ts+199−0
@@ -0,0 +1,199 @@
1import type { CodeModeRunResult, ToolCallReceipt } from "@openassistant/core";
2import { generateText, stepCountIs, tool } from "ai";
3import { getAnthropicModel } from "./anthropic-provider.js";
4import { typecheckCodeSnippet } from "./code-typecheck.js";
5import { z } from "zod";
6
7export interface AgentCodeRun {
8 code: string;
9 result: CodeModeRunResult;
10 reason?: string | undefined;
11}
12
13export interface AgentLoopResult {
14 provider: "claude";
15 planner: string;
16 text: string;
17 runs: AgentCodeRun[];
18}
19
20interface RunCodeToolInput {
21 code: string;
22 reason?: string | undefined;
23}
24
25interface RunCodeToolOutput {
26 ok: boolean;
27 receipts: ToolCallReceipt[];
28 value?: unknown;
29 error?: string;
30}
31
32interface RunWithClaudeInput {
33 prompt: string;
34 now: Date;
35 executeCode: (input: RunCodeToolInput) => Promise<RunCodeToolOutput>;
36}
37
38interface RunWithClaudeOutput {
39 text: string;
40 modelID: string;
41 authSource: string;
42}
43
44interface AgentLoopOptions {
45 now?: Date;
46 runWithClaude?: (input: RunWithClaudeInput) => Promise<RunWithClaudeOutput>;
47}
48
49const DEFAULT_ANTHROPIC_MODEL =
50 readEnv("OPENASSISTANT_ANTHROPIC_MODEL")?.trim() ??
51 readEnv("OPENASSISTANT_CLAUDE_MODEL")?.trim() ??
52 "claude-opus-4-5-20251101";
53const CLAUDE_TIMEOUT_MS = Number(readEnv("OPENASSISTANT_CLAUDE_TIMEOUT_MS") ?? 60_000);
54const CLAUDE_MAX_STEPS = Number(readEnv("OPENASSISTANT_AGENT_MAX_STEPS") ?? 8);
55
56const RUN_CODE_TOOL_SCHEMA = z.object({
57 code: z.string().min(1),
58 reason: z.string().min(1).optional(),
59});
60
61export async function runAgentLoop(
62 prompt: string,
63 runCode: (code: string) => Promise<CodeModeRunResult>,
64 options: AgentLoopOptions = {},
65): Promise<AgentLoopResult> {
66 const now = options.now ?? new Date();
67 const runs: AgentCodeRun[] = [];
68
69 const executeCode = async (input: RunCodeToolInput): Promise<RunCodeToolOutput> => {
70 const typecheck = typecheckCodeSnippet(input.code);
71 if (!typecheck.ok) {
72 const failed: CodeModeRunResult = {
73 ok: false,
74 error: `Typecheck failed: ${typecheck.error}`,
75 receipts: [],
76 };
77 runs.push({
78 code: input.code,
79 result: failed,
80 ...(input.reason ? { reason: input.reason } : {}),
81 });
82 return {
83 ok: false,
84 error: failed.error,
85 receipts: [],
86 };
87 }
88
89 const result = await runCode(input.code);
90 runs.push({
91 code: input.code,
92 result,
93 ...(input.reason ? { reason: input.reason } : {}),
94 });
95
96 if (result.ok) {
97 return {
98 ok: true,
99 value: result.value,
100 receipts: result.receipts,
101 };
102 }
103
104 return {
105 ok: false,
106 error: result.error,
107 receipts: result.receipts,
108 };
109 };
110
111 try {
112 const generated = await (options.runWithClaude ?? runWithClaude)({
113 prompt,
114 now,
115 executeCode,
116 });
117
118 return {
119 provider: "claude",
120 planner: `Claude tool-loop (${runs.length} code run${runs.length === 1 ? "" : "s"}, model=${generated.modelID}, auth=${generated.authSource}).`,
121 text: generated.text,
122 runs,
123 };
124 } catch (error) {
125 return {
126 provider: "claude",
127 planner: `Claude tool-loop failed before completion (model=${DEFAULT_ANTHROPIC_MODEL}).`,
128 text: `Agent loop failed: ${describeUnknown(error)}. No actions were executed.`,
129 runs,
130 };
131 }
132}
133
134async function runWithClaude(input: RunWithClaudeInput): Promise<RunWithClaudeOutput> {
135 const abortController = new AbortController();
136 const timeout = setTimeout(() => abortController.abort(), CLAUDE_TIMEOUT_MS);
137
138 try {
139 const { model, authSource, authMode } = await getAnthropicModel(DEFAULT_ANTHROPIC_MODEL);
140 const result = await generateText({
141 model,
142 temperature: 0,
143 stopWhen: stepCountIs(CLAUDE_MAX_STEPS),
144 prompt: buildAgentPrompt(input.prompt, input.now),
145 system: buildSystemPrompt(authMode),
146 tools: {
147 run_code: tool({
148 description:
149 "Execute Bun TypeScript function body in codemode runtime. Use this for every action that requires tools.* calls.",
150 inputSchema: RUN_CODE_TOOL_SCHEMA,
151 execute: (args) => input.executeCode(args),
152 }),
153 },
154 abortSignal: abortController.signal,
155 });
156
157 return {
158 text: result.text.trim() || "Done.",
159 modelID: DEFAULT_ANTHROPIC_MODEL,
160 authSource,
161 };
162 } finally {
163 clearTimeout(timeout);
164 }
165}
166
167function buildSystemPrompt(authMode: "api" | "oauth"): string {
168 if (authMode === "oauth") {
169 return "You are Claude Code, Anthropic's official CLI for Claude.";
170 }
171
172 return [
173 "You are OpenAssistant. Execute work via the run_code tool and then report what happened.",
174 "In your final response, summarize relevant tool activity and mention failures/denials clearly.",
175 ].join("\n");
176}
177
178function buildAgentPrompt(userPrompt: string, now: Date): string {
179 return [
180 "run_code expects JavaScript function-body code executed as new AsyncFunction('tools', code).",
181 "Inside code, call available tools directly like: await tools.calendar.update({ title, startsAt, notes }).",
182 "For multiple events, produce multiple tool calls in the same code block.",
183 "Never claim an action succeeded unless run_code returned ok=true.",
184 `Current timestamp: ${now.toISOString()}`,
185 `User request: ${userPrompt}`,
186 ].join("\n");
187}
188
189function describeUnknown(error: unknown): string {
190 if (error instanceof Error) {
191 return error.message;
192 }
193 return String(error);
194}
195
196function readEnv(key: string): string | undefined {
197 const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun;
198 return bun?.env?.[key] ?? process.env[key];
199}
apps/gateway/src/anthropic-auth.ts+133−0
@@ -0,0 +1,133 @@
1import { homedir } from "node:os";
2import { join } from "node:path";
3
4const DEFAULT_ANTHROPIC_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
5
6export type ResolvedAnthropicAuth =
7 | {
8 mode: "api";
9 apiKey: string;
10 source: string;
11 }
12 | {
13 mode: "oauth";
14 accessToken: string;
15 refreshToken: string | undefined;
16 expiresAt: number | undefined;
17 clientId: string;
18 source: string;
19 };
20
21export async function resolveAnthropicAuth(): Promise<ResolvedAnthropicAuth | null> {
22 const explicitApiKey = env("OPENASSISTANT_ANTHROPIC_API_KEY") ?? env("ANTHROPIC_API_KEY");
23 if (explicitApiKey) {
24 return {
25 mode: "api",
26 apiKey: explicitApiKey,
27 source: env("OPENASSISTANT_ANTHROPIC_API_KEY") ? "env:OPENASSISTANT_ANTHROPIC_API_KEY" : "env:ANTHROPIC_API_KEY",
28 };
29 }
30
31 const explicitAccess = env("OPENASSISTANT_ANTHROPIC_ACCESS_TOKEN");
32 if (explicitAccess) {
33 const refreshToken = env("OPENASSISTANT_ANTHROPIC_REFRESH_TOKEN");
34 const expiresAt = parseMaybeNumber(env("OPENASSISTANT_ANTHROPIC_EXPIRES_AT"));
35 return {
36 mode: "oauth",
37 accessToken: explicitAccess,
38 refreshToken,
39 expiresAt,
40 clientId: env("OPENASSISTANT_ANTHROPIC_CLIENT_ID") ?? DEFAULT_ANTHROPIC_OAUTH_CLIENT_ID,
41 source: "env:OPENASSISTANT_ANTHROPIC_ACCESS_TOKEN",
42 };
43 }
44
45 const opencodeAuth = await loadOpencodeAnthropicAuth();
46 if (opencodeAuth) {
47 return opencodeAuth;
48 }
49
50 return null;
51}
52
53async function loadOpencodeAnthropicAuth(): Promise<ResolvedAnthropicAuth | null> {
54 const filepath = join(opencodeDataDir(), "auth.json");
55 const file = Bun.file(filepath);
56 const exists = await file.exists();
57 if (!exists) {
58 return null;
59 }
60
61 const raw = (await file.json().catch(() => null)) as Record<string, unknown> | null;
62 if (!raw || typeof raw !== "object") {
63 return null;
64 }
65
66 const anthropic = raw["anthropic"];
67 if (!anthropic || typeof anthropic !== "object") {
68 return null;
69 }
70
71 const record = anthropic as Record<string, unknown>;
72 const type = asString(record["type"]);
73 if (type === "api") {
74 const key = asString(record["key"]);
75 if (!key) {
76 return null;
77 }
78 return {
79 mode: "api",
80 apiKey: key,
81 source: "opencode:auth.json(api)",
82 };
83 }
84
85 if (type === "oauth") {
86 const accessToken = asString(record["access"]);
87 if (!accessToken) {
88 return null;
89 }
90 const refreshToken = asString(record["refresh"]);
91 const expiresAt = asNumber(record["expires"]);
92
93 return {
94 mode: "oauth",
95 accessToken,
96 refreshToken,
97 expiresAt,
98 clientId: env("OPENASSISTANT_ANTHROPIC_CLIENT_ID") ?? DEFAULT_ANTHROPIC_OAUTH_CLIENT_ID,
99 source: "opencode:auth.json(oauth)",
100 };
101 }
102
103 return null;
104}
105
106function opencodeDataDir(): string {
107 const xdg = env("XDG_DATA_HOME");
108 if (xdg) {
109 return join(xdg, "opencode");
110 }
111 return join(homedir(), ".local", "share", "opencode");
112}
113
114function asString(value: unknown): string | undefined {
115 return typeof value === "string" && value.length > 0 ? value : undefined;
116}
117
118function asNumber(value: unknown): number | undefined {
119 return typeof value === "number" && Number.isFinite(value) ? value : undefined;
120}
121
122function parseMaybeNumber(value: string | undefined): number | undefined {
123 if (!value) {
124 return undefined;
125 }
126 const parsed = Number(value);
127 return Number.isFinite(parsed) ? parsed : undefined;
128}
129
130function env(key: string): string | undefined {
131 const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun;
132 return bun?.env?.[key] ?? process.env[key];
133}
apps/gateway/src/anthropic-provider.ts+284−0
@@ -0,0 +1,284 @@
1import { createAnthropic } from "@ai-sdk/anthropic";
2import type { LanguageModel } from "ai";
3import { resolveAnthropicAuth, type ResolvedAnthropicAuth } from "./anthropic-auth.js";
4
5const OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
6const REQUIRED_OAUTH_BETAS = ["oauth-2025-04-20", "interleaved-thinking-2025-05-14"];
7const CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.2 (external, cli)";
8const TOOL_PREFIX = "mcp_";
9
10let cachedAuthPromise: Promise<ResolvedAnthropicAuth | null> | undefined;
11let refreshInFlight: Promise<void> | undefined;
12
13export interface AnthropicModelSelection {
14 model: LanguageModel;
15 authSource: string;
16 authMode: "api" | "oauth";
17}
18
19export async function getAnthropicModel(modelID: string): Promise<AnthropicModelSelection> {
20 const auth = await getResolvedAuth();
21 if (!auth) {
22 throw new Error(
23 "No Anthropic credentials found. Set OPENASSISTANT_ANTHROPIC_API_KEY or OPENASSISTANT_ANTHROPIC_ACCESS_TOKEN (or configure opencode auth).",
24 );
25 }
26
27 if (auth.mode === "api") {
28 const anthropic = createAnthropic({
29 apiKey: auth.apiKey,
30 });
31 return {
32 model: anthropic(modelID),
33 authSource: auth.source,
34 authMode: "api",
35 };
36 }
37
38 const oauthFetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
39 await ensureFreshToken(auth);
40
41 const headers = mergeHeaders(input, init?.headers);
42 headers.set("authorization", `Bearer ${auth.accessToken}`);
43 headers.set("anthropic-beta", mergeBetas(headers.get("anthropic-beta")));
44 headers.set("user-agent", CLAUDE_CLI_USER_AGENT);
45 headers.delete("x-api-key");
46
47 const transformedBody = transformRequestBody(init?.body);
48 const withBetaURL = ensureBetaMessagesURL(input);
49
50 const response = await fetch(withBetaURL, {
51 ...init,
52 headers,
53 ...(transformedBody !== undefined ? { body: transformedBody } : {}),
54 });
55
56 return transformResponseBody(response);
57 }) as typeof fetch;
58
59 const anthropic = createAnthropic({
60 apiKey: "opencode-oauth-dummy-key",
61 fetch: oauthFetch,
62 });
63
64 return {
65 model: anthropic(modelID),
66 authSource: auth.source,
67 authMode: "oauth",
68 };
69}
70
71async function getResolvedAuth(): Promise<ResolvedAnthropicAuth | null> {
72 if (!cachedAuthPromise) {
73 cachedAuthPromise = resolveAnthropicAuth();
74 }
75 return cachedAuthPromise;
76}
77
78async function ensureFreshToken(auth: Extract<ResolvedAnthropicAuth, { mode: "oauth" }>): Promise<void> {
79 const now = Date.now();
80 const expiresAt = auth.expiresAt;
81 const mustRefresh = !auth.accessToken || (typeof expiresAt === "number" && now + 30_000 >= expiresAt);
82 if (!mustRefresh) {
83 return;
84 }
85
86 if (!auth.refreshToken) {
87 throw new Error("Anthropic OAuth access token expired and no refresh token is available.");
88 }
89
90 if (!refreshInFlight) {
91 refreshInFlight = refreshAccessToken(auth).finally(() => {
92 refreshInFlight = undefined;
93 });
94 }
95
96 await refreshInFlight;
97}
98
99async function refreshAccessToken(auth: Extract<ResolvedAnthropicAuth, { mode: "oauth" }>): Promise<void> {
100 const response = await fetch(OAUTH_TOKEN_URL, {
101 method: "POST",
102 headers: {
103 "Content-Type": "application/json",
104 },
105 body: JSON.stringify({
106 grant_type: "refresh_token",
107 refresh_token: auth.refreshToken,
108 client_id: auth.clientId,
109 }),
110 });
111
112 if (!response.ok) {
113 throw new Error(`Anthropic OAuth token refresh failed (${response.status}).`);
114 }
115
116 const json = (await response.json()) as {
117 access_token?: unknown;
118 refresh_token?: unknown;
119 expires_in?: unknown;
120 };
121
122 if (typeof json.access_token !== "string" || json.access_token.length === 0) {
123 throw new Error("Anthropic OAuth token refresh returned no access token.");
124 }
125
126 auth.accessToken = json.access_token;
127 if (typeof json.refresh_token === "string" && json.refresh_token.length > 0) {
128 auth.refreshToken = json.refresh_token;
129 }
130 if (typeof json.expires_in === "number" && Number.isFinite(json.expires_in)) {
131 auth.expiresAt = Date.now() + json.expires_in * 1_000;
132 }
133}
134
135function mergeBetas(existing: string | null): string {
136 const current = (existing ?? "")
137 .split(",")
138 .map((item) => item.trim())
139 .filter((item) => item.length > 0);
140
141 return [...new Set([...REQUIRED_OAUTH_BETAS, ...current])].join(",");
142}
143
144function mergeHeaders(input: RequestInfo | URL, initHeaders: HeadersInit | undefined): Headers {
145 const headers = new Headers();
146
147 if (input instanceof Request) {
148 input.headers.forEach((value, key) => {
149 headers.set(key, value);
150 });
151 }
152
153 if (initHeaders) {
154 const candidate = new Headers(initHeaders);
155 candidate.forEach((value, key) => {
156 headers.set(key, value);
157 });
158 }
159
160 return headers;
161}
162
163function transformRequestBody(body: RequestInit["body"]): RequestInit["body"] {
164 if (typeof body !== "string") {
165 return body;
166 }
167
168 try {
169 const parsed = JSON.parse(body) as Record<string, unknown>;
170 const tools = parsed["tools"];
171 if (Array.isArray(tools)) {
172 parsed["tools"] = tools.map((tool) => {
173 if (!tool || typeof tool !== "object") {
174 return tool;
175 }
176 const candidate = tool as Record<string, unknown>;
177 const name = candidate["name"];
178 if (typeof name === "string" && name.length > 0 && !name.startsWith(TOOL_PREFIX)) {
179 return {
180 ...candidate,
181 name: `${TOOL_PREFIX}${name}`,
182 };
183 }
184 return tool;
185 });
186 }
187
188 const messages = parsed["messages"];
189 if (Array.isArray(messages)) {
190 parsed["messages"] = messages.map((message) => {
191 if (!message || typeof message !== "object") {
192 return message;
193 }
194 const candidate = message as Record<string, unknown>;
195 const content = candidate["content"];
196 if (!Array.isArray(content)) {
197 return message;
198 }
199 return {
200 ...candidate,
201 content: content.map((block) => {
202 if (!block || typeof block !== "object") {
203 return block;
204 }
205 const value = block as Record<string, unknown>;
206 if (value["type"] !== "tool_use") {
207 return block;
208 }
209 const name = value["name"];
210 if (typeof name !== "string" || name.length === 0 || name.startsWith(TOOL_PREFIX)) {
211 return block;
212 }
213 return {
214 ...value,
215 name: `${TOOL_PREFIX}${name}`,
216 };
217 }),
218 };
219 });
220 }
221
222 return JSON.stringify(parsed);
223 } catch {
224 return body;
225 }
226}
227
228function ensureBetaMessagesURL(input: RequestInfo | URL): RequestInfo | URL {
229 let inputIsRequest = false;
230 let parsed: URL | null = null;
231 try {
232 if (input instanceof Request) {
233 inputIsRequest = true;
234 parsed = new URL(input.url);
235 } else if (input instanceof URL) {
236 parsed = new URL(input.toString());
237 } else if (typeof input === "string") {
238 parsed = new URL(input);
239 }
240 } catch {
241 parsed = null;
242 }
243
244 if (!parsed || parsed.pathname !== "/v1/messages" || parsed.searchParams.has("beta")) {
245 return input;
246 }
247
248 parsed.searchParams.set("beta", "true");
249 if (inputIsRequest && input instanceof Request) {
250 return new Request(parsed.toString(), input);
251 }
252 return parsed;
253}
254
255async function transformResponseBody(response: Response): Promise<Response> {
256 if (!response.body) {
257 return response;
258 }
259
260 const reader = response.body.getReader();
261 const decoder = new TextDecoder();
262 const encoder = new TextEncoder();
263
264 const stream = new ReadableStream({
265 async pull(controller) {
266 const { done, value } = await reader.read();
267 if (done) {
268 controller.close();
269 return;
270 }
271
272 const text = decoder
273 .decode(value, { stream: true })
274 .replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
275 controller.enqueue(encoder.encode(text));
276 },
277 });
278
279 return new Response(stream, {
280 status: response.status,
281 statusText: response.statusText,
282 headers: response.headers,
283 });
284}
apps/gateway/src/calendar-store.ts+32−0
@@ -0,0 +1,32 @@
1export interface CalendarEvent {
2 id: string;
3 title: string;
4 startsAt: string;
5 notes?: string;
6}
7
8export interface CalendarUpdateInput {
9 title: string;
10 startsAt: string;
11 notes?: string;
12}
13
14export class InMemoryCalendarStore {
15 private readonly events = new Map<string, CalendarEvent>();
16
17 update(input: CalendarUpdateInput): CalendarEvent {
18 const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
19 const event: CalendarEvent = {
20 id,
21 title: input.title,
22 startsAt: input.startsAt,
23 ...(input.notes ? { notes: input.notes } : {}),
24 };
25 this.events.set(id, event);
26 return event;
27 }
28
29 list(): CalendarEvent[] {
30 return [...this.events.values()];
31 }
32}
apps/gateway/src/code-typecheck.ts+87−0
@@ -0,0 +1,87 @@
1import ts from "typescript";
2
3const VIRTUAL_FILE = "/__openassistant__/codemode-check.ts";
4
5const TOOL_DECLARATIONS = [
6 "type CalendarUpdateInput = { title: string; startsAt: string; notes?: string };",
7 "declare const tools: {",
8 " calendar: {",
9 " update(input: CalendarUpdateInput): Promise<unknown>;",
10 " list(): Promise<unknown>;",
11 " };",
12 "};",
13].join("\n");
14
15export type CodeTypecheckResult =
16 | { ok: true }
17 | { ok: false; error: string };
18
19export function typecheckCodeSnippet(code: string): CodeTypecheckResult {
20 const source = [
21 TOOL_DECLARATIONS,
22 "async function __openassistant_check() {",
23 code,
24 "}",
25 ].join("\n\n");
26
27 const compilerOptions: ts.CompilerOptions = {
28 target: ts.ScriptTarget.ES2022,
29 module: ts.ModuleKind.ESNext,
30 moduleResolution: ts.ModuleResolutionKind.Bundler,
31 strict: true,
32 noEmit: true,
33 skipLibCheck: true,
34 };
35
36 const host = ts.createCompilerHost(compilerOptions, true);
37 const originalGetSourceFile = host.getSourceFile.bind(host);
38 const originalReadFile = host.readFile.bind(host);
39 const originalFileExists = host.fileExists.bind(host);
40
41 host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
42 if (fileName === VIRTUAL_FILE) {
43 return ts.createSourceFile(fileName, source, languageVersion, true);
44 }
45 return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
46 };
47
48 host.readFile = (fileName) => {
49 if (fileName === VIRTUAL_FILE) {
50 return source;
51 }
52 return originalReadFile(fileName);
53 };
54
55 host.fileExists = (fileName) => {
56 if (fileName === VIRTUAL_FILE) {
57 return true;
58 }
59 return originalFileExists(fileName);
60 };
61
62 const program = ts.createProgram([VIRTUAL_FILE], compilerOptions, host);
63 const diagnostics = ts.getPreEmitDiagnostics(program).filter((diagnostic) => {
64 if (!diagnostic.file) {
65 return true;
66 }
67 return diagnostic.file.fileName === VIRTUAL_FILE;
68 });
69
70 if (diagnostics.length === 0) {
71 return { ok: true };
72 }
73
74 const message = diagnostics
75 .slice(0, 3)
76 .map((diagnostic) => {
77 const text = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
78 if (!diagnostic.file || diagnostic.start === undefined) {
79 return text;
80 }
81 const lineAndCharacter = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
82 return `${lineAndCharacter.line + 1}:${lineAndCharacter.character + 1} ${text}`;
83 })
84 .join(" | ");
85
86 return { ok: false, error: message };
87}
apps/gateway/src/index.ts+70−6
@@ -1,10 +1,74 @@
1import { healthcheck } from "@openassistant/core";1import { HttpRouter } from "@effect/platform";
2import { Effect } from "effect";2import { BunHttpServer, BunRuntime } from "@effect/platform-bun";
3import { RpcSerialization, RpcServer } from "@effect/rpc";
4import { AgentRpcs, RunTurnOutput, createCodeModeRunner } from "@openassistant/core";
5import { Effect, Layer } from "effect";
6import { runAgentLoop } from "./agent-loop.js";
7import { InMemoryCalendarStore } from "./calendar-store.js";
8import { createToolTree } from "./tools.js";
3 9
4const program = Effect.flatMap(healthcheck, (status) =>10const port = Number(readEnv("OPENASSISTANT_GATEWAY_PORT") ?? "8787");
5 Effect.sync(() => {11
6 console.log(`[gateway] healthcheck=${status}`);12const calendarStore = new InMemoryCalendarStore();
13const tools = createToolTree(calendarStore);
14const runner = createCodeModeRunner({
15 tools,
16 requestApproval: () => Effect.succeed("approved"),
17});
18
19const AgentHandlersLive = AgentRpcs.toLayer(
20 Effect.succeed({
21 RunTurn: (input: { prompt: string; nowIso: string }) =>
22 Effect.tryPromise({
23 try: async () => {
24 const generated = await runAgentLoop(
25 input.prompt,
26 (code) => Effect.runPromise(runner.run({ code })),
27 {
28 now: new Date(input.nowIso),
29 },
30 );
31
32 return new RunTurnOutput({
33 message: generated.text,
34 planner: generated.planner,
35 codeRuns: generated.runs.length,
36 ...(isVerboseMode() ? { footer: generated.planner } : {}),
37 });
38 },
39 catch: (error) => `RunTurn failed: ${describeUnknown(error)}`,
40 }),
7 }),41 }),
8);42);
9 43
10await Effect.runPromise(program);44const RpcLayer = RpcServer.layer(AgentRpcs).pipe(Layer.provide(AgentHandlersLive));
45
46const HttpProtocolLayer = RpcServer.layerProtocolHttp({
47 path: "/rpc",
48}).pipe(Layer.provide(RpcSerialization.layerNdjson));
49
50const MainLayer = HttpRouter.Default.serve().pipe(
51 Layer.provide(RpcLayer),
52 Layer.provide(HttpProtocolLayer),
53 Layer.provide(BunHttpServer.layer({ port })),
54);
55
56console.log(`[gateway] listening on http://localhost:${port}/rpc`);
57BunRuntime.runMain(Layer.launch(MainLayer));
58
59function describeUnknown(error: unknown): string {
60 if (error instanceof Error) {
61 return error.message;
62 }
63 return String(error);
64}
65
66function isVerboseMode(): boolean {
67 const value = readEnv("OPENASSISTANT_VERBOSE_RESPONSE")?.trim().toLowerCase();
68 return value === "1" || value === "true" || value === "yes";
69}
70
71function readEnv(key: string): string | undefined {
72 const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun;
73 return bun?.env?.[key] ?? process.env[key];
74}
apps/gateway/src/tools.ts+22−0
@@ -0,0 +1,22 @@
1import { defineTool, type ToolTree } from "@openassistant/core";
2import { Effect } from "effect";
3import type { InMemoryCalendarStore } from "./calendar-store.js";
4
5export function createToolTree(calendarStore: InMemoryCalendarStore): ToolTree {
6 return {
7 calendar: {
8 update: defineTool({
9 kind: "write",
10 approval: "required",
11 run: (input: { title: string; startsAt: string; notes?: string }) =>
12 Effect.sync(() => calendarStore.update(input)),
13 previewInput: (input) => `${input.title} @ ${input.startsAt}`,
14 }),
15 list: defineTool({
16 kind: "read",
17 approval: "auto",
18 run: () => Effect.sync(() => calendarStore.list()),
19 }),
20 },
21 };
22}
bun.lock+73−2
@@ -17,6 +17,8 @@ · 16 unmodified lines
17 "name": "@openassistant/discord-bot",17 "name": "@openassistant/discord-bot",
18 "dependencies": {18 "dependencies": {
19 "@ai-sdk/anthropic": "^2.0.0",19 "@ai-sdk/anthropic": "^2.0.0",
20 "@effect/platform": "latest",
21 "@effect/rpc": "latest",
20 "@openassistant/core": "workspace:*",22 "@openassistant/core": "workspace:*",
21 "@openassistant/reacord": "workspace:*",23 "@openassistant/reacord": "workspace:*",
22 "ai": "^6.0.73",24 "ai": "^6.0.73",
@@ -32,13 +34,20 @@ · 9 unmodified lines
32 "apps/gateway": {34 "apps/gateway": {
33 "name": "@openassistant/gateway",35 "name": "@openassistant/gateway",
34 "dependencies": {36 "dependencies": {
37 "@ai-sdk/anthropic": "^2.0.0",
38 "@effect/platform": "latest",
39 "@effect/platform-bun": "latest",
40 "@effect/rpc": "latest",
35 "@openassistant/core": "workspace:*",41 "@openassistant/core": "workspace:*",
42 "ai": "^6.0.73",
36 "effect": "latest",43 "effect": "latest",
44 "zod": "^4.3.6",
37 },45 },
38 },46 },
39 "packages/core": {47 "packages/core": {
40 "name": "@openassistant/core",48 "name": "@openassistant/core",
41 "dependencies": {49 "dependencies": {
50 "@effect/rpc": "latest",
42 "effect": "latest",51 "effect": "latest",
43 },52 },
44 },53 },
@@ -58,6 +67,22 @@ · 13 unmodified lines
58 },67 },
59 },68 },
60 },69 },
70 "overrides": {
71 "@ai-sdk/anthropic": "^2.0.0",
72 "@effect-atom/atom": "^0.4.11",
73 "@effect-atom/atom-react": "^0.4.4",
74 "@effect/platform": "latest",
75 "@effect/platform-bun": "latest",
76 "@effect/rpc": "latest",
77 "@types/react": "^19.0.0",
78 "@types/react-reconciler": "^0.31.0",
79 "ai": "^6.0.73",
80 "discord.js": "^14.17.3",
81 "effect": "latest",
82 "react": "^19.0.0",
83 "react-reconciler": "^0.31.0",
84 "zod": "^4.3.6",
85 },
61 "packages": {86 "packages": {
62 "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.59", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-s7T5etVW7k4y3p1TDpTGa7xBddvSPWTvRa+EHAI0MPCrVh2dVFmAk+Dm6I8zlr1vz45SBpHvKR3SP29aGXdrUQ=="],87 "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.59", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-s7T5etVW7k4y3p1TDpTGa7xBddvSPWTvRa+EHAI0MPCrVh2dVFmAk+Dm6I8zlr1vz45SBpHvKR3SP29aGXdrUQ=="],
63 88
@@ -83,16 +108,26 @@ · 19 unmodified lines
83 108
84 "@effect-atom/atom-react": ["@effect-atom/atom-react@0.4.6", "", { "dependencies": { "@effect-atom/atom": "^0.4.13" }, "peerDependencies": { "effect": "^3.19", "react": ">=18 <20", "scheduler": "*" } }, "sha512-ZvncuSQsCcUrsddD00JcKDMraPge0AiDMGiJqKc7ProvagCVrxB+Emy5uHM0l0SLGUx4X56bFE0d1tp9Z/Bf+g=="],109 "@effect-atom/atom-react": ["@effect-atom/atom-react@0.4.6", "", { "dependencies": { "@effect-atom/atom": "^0.4.13" }, "peerDependencies": { "effect": "^3.19", "react": ">=18 <20", "scheduler": "*" } }, "sha512-ZvncuSQsCcUrsddD00JcKDMraPge0AiDMGiJqKc7ProvagCVrxB+Emy5uHM0l0SLGUx4X56bFE0d1tp9Z/Bf+g=="],
85 110
111 "@effect/cluster": ["@effect/cluster@0.56.2", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.94.3", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "@effect/workflow": "^0.16.0", "effect": "^3.19.16" } }, "sha512-BtYorswZ0MlaTqa7k+57AD80gp/TJfej85vx0gDnIxhbPQ3+xoa3EXlPrfLV7XsiAokOxE5qs/X6UoZtAVeKug=="],
112
86 "@effect/experimental": ["@effect/experimental@0.57.11", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.93.6", "effect": "^3.19.9", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-M5uug3Drs/gyTHLfA+XzcIZQGUEV/Jn5yi1POki4oZswhpzNmsVTHl4THpxAordRKwa5lFvTSlsRP684YH7pSw=="],113 "@effect/experimental": ["@effect/experimental@0.57.11", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.93.6", "effect": "^3.19.9", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-M5uug3Drs/gyTHLfA+XzcIZQGUEV/Jn5yi1POki4oZswhpzNmsVTHl4THpxAordRKwa5lFvTSlsRP684YH7pSw=="],
87 114
88 "@effect/language-service": ["@effect/language-service@0.73.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-/SGoq50VDm/XwQI6d0cReYcLjwfdqwZv7uEJp92+ssx5vLCsm+QvHf4Ul6l6PYVzorsgAp/b6fhAwI2VSkqcJQ=="],115 "@effect/language-service": ["@effect/language-service@0.73.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-/SGoq50VDm/XwQI6d0cReYcLjwfdqwZv7uEJp92+ssx5vLCsm+QvHf4Ul6l6PYVzorsgAp/b6fhAwI2VSkqcJQ=="],
89 116
90 "@effect/platform": ["@effect/platform@0.93.8", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.12" } }, "sha512-xTEy6fyTy4ijmFC3afKgtvYtn/JyPoIov4ZSUWJZUv3VeOcUPNGrrqG6IJlWkXs3NhvSywKv7wc1kw3epCQVZw=="],117 "@effect/platform": ["@effect/platform@0.94.3", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.16" } }, "sha512-bvTR8xLQoRpKgHuprZDOeQdPkhyVw+WT05iI9jl2s8Qiblyk5Dz2JLwJU+EFeksIBaPYz49xa635Om91T1CefQ=="],
118
119 "@effect/platform-bun": ["@effect/platform-bun@0.87.1", "", { "dependencies": { "@effect/platform-node-shared": "^0.57.1", "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-I88d0YqWbvLY2GGeIxK3r5k0l/MoUCCnxiHJG+X6gqaHu+pIs0djDtJ+ORhw/3qha9ojcVu6pyaBmnUjgzQHWQ=="],
120
121 "@effect/platform-node-shared": ["@effect/platform-node-shared@0.57.1", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "multipasta": "^0.2.7", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag=="],
91 122
92 "@effect/rpc": ["@effect/rpc@0.72.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.93.3", "effect": "^3.19.5" } }, "sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ=="],123 "@effect/rpc": ["@effect/rpc@0.73.0", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-iMPf6tTriz8sK0l5x4koFId8Hz5nFptHYg8WqyjHGIIVLTpZxuiSqhmXZG7FnAs5N2n6uCEws4wWGcIgXNUrFg=="],
124
125 "@effect/sql": ["@effect/sql@0.49.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA=="],
93 126
94 "@effect/vitest": ["@effect/vitest@0.27.0", "", { "peerDependencies": { "effect": "^3.19.0", "vitest": "^3.2.0" } }, "sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ=="],127 "@effect/vitest": ["@effect/vitest@0.27.0", "", { "peerDependencies": { "effect": "^3.19.0", "vitest": "^3.2.0" } }, "sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ=="],
95 128
129 "@effect/workflow": ["@effect/workflow@0.16.0", "", { "peerDependencies": { "@effect/experimental": "^0.58.0", "@effect/platform": "^0.94.0", "@effect/rpc": "^0.73.0", "effect": "^3.19.13" } }, "sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw=="],
130
96 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],131 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
97 132
98 "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],133 "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -169,6 +204,34 @@ · 70 unmodified lines
169 204
170 "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],205 "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
171 206
207 "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
208
209 "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
210
211 "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
212
213 "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
214
215 "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
216
217 "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
218
219 "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
220
221 "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
222
223 "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
224
225 "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
226
227 "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
228
229 "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
230
231 "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
232
233 "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
234
172 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],235 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
173 236
174 "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],237 "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
@@ -323,10 +386,16 @@ · 148 unmodified lines
323 386
324 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],387 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
325 388
389 "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
390
391 "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
392
326 "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],393 "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
327 394
328 "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],395 "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
329 396
397 "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
398
330 "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],399 "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
331 400
332 "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],401 "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
@@ -347,6 +416,8 @@ · 14 unmodified lines
347 416
348 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],417 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
349 418
419 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
420
350 "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],421 "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
351 422
352 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],423 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
package.json+17−0
@@ -16,6 +16,7 @@ · 15 unmodified lines
16 "test": "vitest run",16 "test": "vitest run",
17 "test:watch": "vitest",17 "test:watch": "vitest",
18 "clean": "bun run --filter '@openassistant/core' clean && bun run --filter '@openassistant/reacord' clean && bun run --filter '@openassistant/gateway' clean && bun run --filter '@openassistant/discord-bot' clean",18 "clean": "bun run --filter '@openassistant/core' clean && bun run --filter '@openassistant/reacord' clean && bun run --filter '@openassistant/gateway' clean && bun run --filter '@openassistant/discord-bot' clean",
19 "dev": "bun run dev:gateway & bun run dev:discord & wait",
19 "dev:gateway": "bun run --cwd apps/gateway dev",20 "dev:gateway": "bun run --cwd apps/gateway dev",
20 "dev:discord": "bun run --cwd apps/discord-bot dev"21 "dev:discord": "bun run --cwd apps/discord-bot dev"
21 },22 },
@@ -26,5 +27,21 @@ · 4 unmodified lines
26 "@typescript/native-preview": "7.0.0-dev.20260205.1",27 "@typescript/native-preview": "7.0.0-dev.20260205.1",
27 "typescript": "latest",28 "typescript": "latest",
28 "vitest": "^3.2.0"29 "vitest": "^3.2.0"
30 },
31 "overrides": {
32 "@ai-sdk/anthropic": "^2.0.0",
33 "@effect/platform": "latest",
34 "@effect/platform-bun": "latest",
35 "@effect/rpc": "latest",
36 "@effect-atom/atom": "^0.4.11",
37 "@effect-atom/atom-react": "^0.4.4",
38 "@types/react": "^19.0.0",
39 "@types/react-reconciler": "^0.31.0",
40 "ai": "^6.0.73",
41 "discord.js": "^14.17.3",
42 "effect": "latest",
43 "react": "^19.0.0",
44 "react-reconciler": "^0.31.0",
45 "zod": "^4.3.6"
29 }46 }
30}47}
packages/core/package.json+1−0
@@ -12,6 +12,7 @@ · 11 unmodified lines
12 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""12 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""
13 },13 },
14 "dependencies": {14 "dependencies": {
15 "@effect/rpc": "latest",
15 "effect": "latest"16 "effect": "latest"
16 }17 }
17}18}
packages/core/src/agent-rpc.ts+24−0
@@ -0,0 +1,24 @@
1import { Rpc, RpcGroup } from "@effect/rpc";
2import { Schema } from "effect";
3
4export class RunTurnInput extends Schema.Class<RunTurnInput>("RunTurnInput")({
5 prompt: Schema.String,
6 requesterId: Schema.String,
7 channelId: Schema.String,
8 nowIso: Schema.String,
9}) {}
10
11export class RunTurnOutput extends Schema.Class<RunTurnOutput>("RunTurnOutput")({
12 message: Schema.String,
13 planner: Schema.String,
14 codeRuns: Schema.Number,
15 footer: Schema.optional(Schema.String),
16}) {}
17
18export class AgentRpcs extends RpcGroup.make(
19 Rpc.make("RunTurn", {
20 payload: RunTurnInput,
21 success: RunTurnOutput,
22 error: Schema.String,
23 }),
24) {}
packages/core/src/index.ts+1−0
@@ -2,3 +2,4 @@ import { Effect } from "effect"; · 1 unmodified line
2 2
3export const healthcheck = Effect.succeed("ok");3export const healthcheck = Effect.succeed("ok");
4export * from "./codemode/runner.js";4export * from "./codemode/runner.js";
5export * from "./agent-rpc.js";