diff --git a/plugins/cef/.claude-plugin/plugin.json b/plugins/cef/.claude-plugin/plugin.json index 0dd976d..8b2068b 100644 --- a/plugins/cef/.claude-plugin/plugin.json +++ b/plugins/cef/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "cef", "description": "Author, test, build, push, and deploy CEF agents", - "version": "0.1.0", + "version": "0.1.5", "author": { "name": "Cere / CEF-AI" }, "homepage": "https://github.com/CEF-AI/cef-plugins" } diff --git a/plugins/cef/skills/deploy/SKILL.md b/plugins/cef/skills/deploy/SKILL.md index b0e9cbb..2edd026 100644 --- a/plugins/cef/skills/deploy/SKILL.md +++ b/plugins/cef/skills/deploy/SKILL.md @@ -16,7 +16,8 @@ deployed, not because it is published. In a scaffolded project run `cef` via `npx cef …` (it's a local dev-dep). ```bash -npx cef init # scaffold a project (ships a deployments/ folder) +npx cef init my-agent # scaffold (does NOT install deps — fast) +cd my-agent && pnpm install # install project deps (incl. the `cef` dev-dep) npx cef build # local: compile dist//{bundle.js,manifest.json} npx cef push --bucket --as-pubkey # OUTWARD: upload bundle to the DDC registry npx cef deploy --endpoint --as-pubkey # OUTWARD: apply deployments/ — makes the agent live diff --git a/plugins/cef/skills/develop/SKILL.md b/plugins/cef/skills/develop/SKILL.md index ea144c7..581537b 100644 --- a/plugins/cef/skills/develop/SKILL.md +++ b/plugins/cef/skills/develop/SKILL.md @@ -44,6 +44,7 @@ to lint + bundle. | Shipping a static UI | [widgets.md](./references/widgets.md) | `WidgetDecl`, self-contained dirs, `cef push` DAG, `window.WidgetRuntime` | | Writing tests | [testing.md](./references/testing.md) | `testAgent`, `testPlatform`, model mocks, `dispatch`/`published`, harness members | | Debugging a build failure or lint error | [constraints.md](./references/constraints.md) | banned imports, string-literal + declared-alias rules, ESLint wiring | +| Debugging a running agent (empty logs, stuck Job, cubby errors) | [troubleshooting.md](./references/troubleshooting.md) | where logs go, logging that surfaces the cause, guard handlers, local repro with `testAgent`, common failure modes | ## The shape at a glance @@ -68,11 +69,12 @@ export default class Echo { @OnEvent("user_message") async onMessage(event: Event<{ text: string }>, ctx: Context) { + const now = Date.now(); await ctx.cubby("history").exec( "INSERT INTO messages(id, text, ts) VALUES (?, ?, ?)", - [Date.now(), event.payload.text, event.ts], + [now, event.payload.text, now], ); - await ctx.publish("ack", { for: event.id }); + await ctx.publish("ack", { text: `got: ${event.payload.text}` }); } } ``` @@ -96,11 +98,13 @@ export default class Echo { ## Dev commands In a scaffolded project `cef` is a local dev-dependency, so run it via `npx cef …` -(bare `cef` is not on `PATH`). A `cef init` project also aliases the common ones -as npm scripts (`pnpm run build`, `pnpm test`). +(bare `cef` is not on `PATH`). `cef init` does not install — run `pnpm install` +once first. The project also aliases the common commands as npm scripts +(`pnpm run build`, `pnpm test`, `pnpm dev`). ```sh npx cef typegen # after editing cef.config.ts: refresh .cef/generated.d.ts + cef.lock.json +npx cef dev [widgetId] # local widget dev server: standalone browser auth + file-watch npx cef build # lint entry + bundle → dist//{bundle.js, manifest.json} npx cef inspect dist/ # verify exposed __handlers match manifest routing npx vitest run # run the @cef-ai/testing simulator tests (or: pnpm test) diff --git a/plugins/cef/skills/develop/references/engagements.md b/plugins/cef/skills/develop/references/engagements.md index 4c864d3..0f93edd 100644 --- a/plugins/cef/skills/develop/references/engagements.md +++ b/plugins/cef/skills/develop/references/engagements.md @@ -31,11 +31,12 @@ export default class Echo { @OnEvent("user_message") async onMessage(event: Event, ctx: Context) { + const now = Date.now(); await ctx.cubby("history").exec( "INSERT INTO messages(id, text, ts) VALUES (?, ?, ?)", - [Date.now(), event.payload.text, event.ts], + [now, event.payload.text, now], ); - await ctx.publish("ack", { for: event.id }); + await ctx.publish("ack", { text: `got: ${event.payload.text}` }); } @OnClose @@ -78,14 +79,20 @@ type per engagement. ```ts interface Event

{ - id: string; // event id - type: string; // event type string - ts: number; // unix-millis timestamp - from: string; // publisher (agent id or external source) - payload: P; // typed body + type: string; // event type string, e.g. "user_message" + payload: P; // typed body + timestamp: string; // ISO-8601, assigned by the vault + context?: string; // stream context (conversation / partition key) + role?: string; // publisher role, e.g. "user" | "agent" + eventId?: string; // server-assigned id (UUID v7); may be absent inbound } ``` +The runtime hands the handler the **published event verbatim**, so read +`event.payload` (typed) and `event.timestamp` (a string — `Date.parse(...)` for +millis). There is no numeric `ts`; don't bind a missing field into a NOT NULL +cubby column — use `Date.now()` for a record time, as above. + Type the payload with your own interface: `Event`, then read `event.payload.text`. diff --git a/plugins/cef/skills/develop/references/troubleshooting.md b/plugins/cef/skills/develop/references/troubleshooting.md new file mode 100644 index 0000000..ad234dd --- /dev/null +++ b/plugins/cef/skills/develop/references/troubleshooting.md @@ -0,0 +1,80 @@ +# Troubleshooting & debugging a CEF agent + +Agents run server-side per **Job**. You can't attach a debugger, so **logging +and the local simulator are your two windows**. Design for that from the start. + +## Where output goes + +`console.log` / `console.info` / `console.error` from your agent go to the +**sandbox console → the platform/ROC logs**. That is the only runtime signal +from a deployed agent. There is no `ctx.log` — use `console.*`. + +## Logging that is actually debuggable + +- **Fold the error text into the message string.** The sandbox captures the + **first** console arg as the log `message` (via `.String()`) and + **JSON-serializes the rest** into a separate `args` field. The gotcha: + `JSON.stringify(new Error(...))` is `"{}"` — an `Error`'s `message`/`stack` + are non-enumerable — so passing the error as a *second* arg records an empty + `{}` and hides the cause: + + ```ts + console.error("failed to persist message", err); // ✗ args: [{}] — err lost + ``` + + Put the detail in the first arg, where `.String()` preserves it + (`String(anError)` → `"Error: "`): + + ```ts + const detail = err instanceof Error ? err.message : String(err); + console.error(`[history] failed to persist message: ${detail}`); // ✓ + ``` + + (A *string* or plain object as a later arg serializes fine — it's specifically + `Error` objects that collapse to `{}`.) + +- **Prefix logs** with the subsystem (`[history]`, `[reply]`) so they're + greppable in a busy log. +- **Log enough context** to reproduce (which operation, which keys/ids) — but + never secrets or user PII. + +## Handlers must not throw + +An unhandled error in an `@OnEvent` / `@OnStart` handler can **wedge the Job** +(its queued tasks get stuck). Treat every handler as adversarial-input-facing: + +- **Guard the payload** — it can be missing, empty, or the wrong shape. Validate + before use; reply with a clear message instead of throwing. +- **Wrap side effects** (cubby writes, `publish`, model calls) in `try/catch`, + log the real error, and degrade gracefully. + +## Debug locally FIRST — the simulator + +Before push → deploy → read-logs, reproduce with **`@cef-ai/testing`** +(`testAgent`). It runs your handler in-process and surfaces the **real error + +stack trace immediately** — the fastest loop by far. + +```ts +import { testAgent } from "@cef-ai/testing"; +// drive the exact event that failed; assert the cubby / reply; read the throw +``` + +Run `pnpm test` (or `npx vitest run`) and add a case for the failing event. + +## Common failure modes + +| Symptom (in logs) | Cause | Fix | +|---|---|---| +| `UNIQUE constraint failed: .` | Inserting a non-unique value into a PK/unique column (e.g. `Date.now()` as `id` collides same-ms) | Let SQLite assign `INTEGER PRIMARY KEY` (omit the id), or use a genuinely unique key | +| `NOT NULL constraint failed:
.` | A bound value was `undefined`/`null` | Guard/default the value before the write | +| Widget: `AgentNotConnectedError` | The agent isn't connected for that user | Render a Connect CTA wired to `WidgetRuntime.connectAgent()`, then retry | +| Job stuck / no reply | An unguarded `throw` in a handler | Guard payloads; `try/catch` side effects; always `publish` a reply path | +| Types don't match the model / event | `cef.config.ts` changed but types are stale | Run `cef typegen` | +| `ctx.cubby(...)`: no orchestrator endpoint | Running the handler outside a Job context | Exercise it via `testAgent`, not by calling the class directly | + +## Inspect state + +- **Read the cubby** with a `query` (in a widget or a `testAgent` assertion) to + see what's actually stored. +- **`cef inspect dist/`** verifies the built bundle's handlers match the + manifest routing (catches "event handled but nothing happened"). diff --git a/plugins/cef/skills/develop/references/widgets.md b/plugins/cef/skills/develop/references/widgets.md index b72c7c3..553ea43 100644 --- a/plugins/cef/skills/develop/references/widgets.md +++ b/plugins/cef/skills/develop/references/widgets.md @@ -1,106 +1,283 @@ # Widgets -A **widget** is a self-contained static web UI (an entry HTML plus sibling JS/assets) that an agent ships alongside its bundle. You declare it in `cef.config.ts`, build it into a plain directory, and `cef push` uploads the whole directory to DDC as one content-addressed **directory DAG** — next to `bundle.js`. There is no baked gateway URL: consumers resolve the widget at `///`. +A **widget** is a small web UI an agent ships alongside its bundle. At runtime a +widget reads and writes the **viewing user's own vault** — as the connected +agent — through an injected `window.WidgetRuntime`. No secret is baked into the +widget; the user's key material stays in their wallet. The same widget renders +two ways: **embedded** inside a host (the Cere Vault) and **standalone** via a +direct link. Both talk to the SAME agent's cubby for the SAME user. + +You author a widget as source, iterate against your dev vault with `cef dev`, +and ship it with `cef build` + `cef push`. + +> `cef dev` and `cef build`'s widget baking require a recent `@cef-ai/cli`. +> Run `cef --help` to confirm the flags available in your version. + +## The dev loop + +```sh +cef init my-agent # scaffolds an agent with a real, queryable widget +cd my-agent && pnpm install + +cef dev hello # local dev server for the `hello` widget +# - serves the widget on localhost +# - wires the manifest + the widget runtime +# - interactive wallet connect (a Cere wallet popup) — same auth as production standalone +# - watches files → live-reloads the browser on save + +# iterate: edit widgets/hello/*, save → the browser reloads automatically + +cef build --env dev # bakes the manifest + runtime +Candidate List +
Loading…
+ ``` -`app.js`: +`candidates.js`: + ```js -document.getElementById("root").textContent = "hello from widget"; +const root = document.getElementById("root"); + +async function render() { + try { + // "recent" is the query id declared in cef.config.ts; 20 binds to the ? placeholder. + const { columns, rows } = await window.WidgetRuntime.query("recent", [20]); + const name = columns.indexOf("name"); + const stage = columns.indexOf("stage"); + root.innerHTML = rows.length + ? rows.map((r) => `
${r[name]} — ${r[stage]}
`).join("") + : "No candidates yet."; + } catch (err) { + if (err.name === "AgentNotConnectedError") { + renderConnectCta(); // the agent isn't connected for this user yet + } else { + root.textContent = `Error: ${err.message}`; + } + } +} + +function renderConnectCta() { + root.innerHTML = ``; + document.getElementById("connect").onclick = async () => { + await window.WidgetRuntime.connectAgent(); // installs the agent for this user + render(); + }; +} + +render(); ``` -That's a complete, valid widget. Keep the starter widget plain like this — do not pull in the widget runtime until you actually need Vault access. +A `record` widget is the same shape with a query that returns a single row; a +`submit` widget collects form input and calls +`window.WidgetRuntime.publish("candidate_added", { name, stage })`. -> The CLI does NOT bundle or transform your widget. `dir` must already be built by the time you run `cef push`. If your widget needs a build step (bundler, TS), run it as the agent's own `build:widgets` script before `cef build`. +### Connecting the agent (`connectAgent`) -## How `cef build` and `cef push` handle it +`query()` and `publish()` require the agent to be **connected** for the viewing +user. When it isn't, `query()` throws `AgentNotConnectedError` — catch it and +render a CTA wired to `connectAgent()`. `connectAgent()` is the one-time +"install" flow: it connects the agent to the user's vault for the widget's +scope. After it resolves, retry the query. Use `agentStatus()` to check state up +front and `onIdentityChange(cb)` to re-render when the user connects, +disconnects, or the agent's status changes. -- **`cef build`** copies whatever is in each `dir` into `dist//widgets//` (fresh each build — stale files are removed first). It also emits one `ManifestWidget` per declared widget with a **placeholder** `dag: { bucketId: "", cid: "" }`. If `dir` is missing, or the `entry` file isn't found inside it, build prints a WARNING but still emits the manifest entry. -- **`cef push`** reads each staged `dist//widgets//` directory, uploads it to DDC as one content-addressed directory DAG, and stamps the real `{ bucketId, cid }` into that widget's manifest entry. A widget with no staged directory (or an empty one) is skipped with a WARNING. +## Embedded vs standalone -So the flow is: `build:widgets` (your own) → `cef build` (stage into dist) → `cef push` (upload as DAG + stamp ref). +The same widget runs in two modes; you write it once and the runtime handles the +difference. -## Optional: `window.WidgetRuntime` (Vault-native widgets) +- **Embedded** — the widget runs inside a host (the Cere Vault). The host + supplies identity and signing over a bridge; the user is already + authenticated, so `connect()` resolves immediately. +- **Standalone** — the widget is opened via a direct link. The runtime + authenticates the user itself with an interactive Cere wallet connect (a + popup). `cef dev` runs in this mode, so local iteration matches production + standalone. -When a widget is loaded inside the Cere Vault host, an injected runtime (`@cef-ai/widget-runtime`, emerging) exposes `window.WidgetRuntime` for reading/writing the **viewing user's** own Vault as the connected agent — no baked secret, key material stays in the wallet iframe. Core surface: +In both modes the widget queries the **same agent's cubby for the same user** — +the only difference is where the identity comes from. Don't assume a host is +present: rely on `WidgetRuntime` (which works in both) rather than reaching for +host-specific globals. -```ts -window.WidgetRuntime.query(ref, params?) // ref = a named query id (from queries[]) or raw SQL - // → { columns, rows, meta }; params are BOUND, never interpolated -window.WidgetRuntime.publish(type, payload, context?) // → { eventId } -window.WidgetRuntime.connectAgent() // the "install" flow: getAgent + vault.agents.connect() -window.WidgetRuntime.identity() / .connect() / .agentStatus() / .onIdentityChange(cb) -``` +## The manifest + +`cef build` produces a **widget manifest** that the runtime reads at boot. It +carries everything the widget needs to resolve identity, connect the agent, and +run its queries — you don't assemble it by hand: + +- `widgetId`, `name`, `kind`, `config` — from your `WidgetDecl`. +- `agentId`, `scope`, `cubbyAlias` — which agent/cubby the widget binds to. +- `queries[]` — `{ id, label, sql, timeoutMs }` from your declaration. +- `wallet` — `{ appId, env }` used to build the interactive connect in standalone. +- `endpoints` — the resolved `{ vault, gar, marketplace, s3GatewayAuthInfo }` + for the built `--env`. + +The runtime **reads** a manifest; it never builds one. The CLI owns manifest +construction (from `cef.config.ts` + `--env` + the agent id), which is why the +same widget source works locally under `cef dev` and in production after +`cef build`. + +### What must be in the manifest vs what can live in the widget JS + +**Connection + identity must be in the manifest** — the runtime needs them +*before* your JS runs (it builds the vault client + authenticates first): +`endpoints`, `wallet`, `agentId`. These can't move into the widget logic, +because the logic depends on them already being resolved. -`query()` throws `AgentNotConnectedError` when the agent isn't connected for this user — catch it to render a "Connect / Install" CTA wired to `connectAgent()`. +**`cubbyAlias` is the widget's cubby binding.** It's optional if the widget +never reads a cubby, but `query()` takes **no per-call cubby argument** — it +always runs against `manifest.cubbyAlias` — so a widget that *does* query must +declare its one cubby here. (You can't select the cubby from the `query()` call +today.) -Gotchas: -- The runtime is injected by the upload/host step, not by `cef push`. Opened directly in a browser (no Vault host) it assigns `window.WidgetRuntime` synchronously but `query()`/`publish()` reject after an ~8s handshake deadline and render "Open this widget inside the Cere Vault." -- Keep starter/hello-world widgets plain and self-contained. Only reach for the runtime once the widget genuinely needs Vault data. +**`queries[]` are a choice.** Declaring them powers the zero-JS declarative kinds +(`list`/`record`/`dashboard`) and makes a widget's data access reviewable. But +`WidgetRuntime.query()` also accepts **raw SQL**, so a `kind: "custom"` widget +can keep its query logic in the JS — `query("SELECT text, ts FROM messages …")` +— with no `queries[]` at all. Either way the boundary that matters is the +**scope** (agent + cubby + user), enforced by the runtime regardless of the SQL. ## Gotchas checklist -- `id`, `dir`, and `entry` are all required; `id` must be unique within the agent (build errors on duplicates). -- `dir` must be **built already** — the CLI copies, it does not compile or bundle. -- Reference siblings with `./relative` paths only; absolute/external URLs won't resolve under the directory CID. -- A missing `dir` or `entry` produces a WARNING (not an error) at build; `cef push` then skips that widget — so a "successful" build can still ship no widget. Check push output for `widget : dir DAG ( files)`. -- No gateway URL is baked in; consumers build `///` from the manifest ref. +- **`kind` is required, and non-`custom` kinds need `queries`.** A query-backed + widget with no `queries` (or no `cubbyAlias`) has nothing to render. +- **Query refs are ids or raw SQL, and params are bound.** Pass values in the + `params` array against `?` placeholders; never interpolate into SQL. +- **Handle `AgentNotConnectedError`.** First load for a new user throws it — + render a `connectAgent()` CTA instead of an error. +- **Endpoints are build-time baked from `--env`.** Build for the environment you + intend to run in; there is no runtime environment switch. +- **Don't depend on a host.** Standalone has no host bridge — use + `WidgetRuntime`, not host-only globals, so the widget works in both modes. +- **Reference siblings with `./relative` paths.** The widget is served as one + directory; absolute/external URLs won't resolve. +- **`id` must be unique within the agent** and is what `cef dev [widgetId]` + targets.