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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/cef/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 2 additions & 1 deletion plugins/cef/skills/deploy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/{bundle.js,manifest.json}
npx cef push --bucket <id> --as-pubkey <hex> # OUTWARD: upload bundle to the DDC registry
npx cef deploy --endpoint <url> --as-pubkey <hex> # OUTWARD: apply deployments/ — makes the agent live
Expand Down
12 changes: 8 additions & 4 deletions plugins/cef/skills/develop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}` });
}
}
```
Expand All @@ -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/<agentId>/{bundle.js, manifest.json}
npx cef inspect dist/<agentId> # verify exposed __handlers match manifest routing
npx vitest run # run the @cef-ai/testing simulator tests (or: pnpm test)
Expand Down
21 changes: 14 additions & 7 deletions plugins/cef/skills/develop/references/engagements.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export default class Echo {

@OnEvent("user_message")
async onMessage(event: Event<UserMessage>, 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}` });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Good — the corrected ack payload typechecks against the rewritten Event<P> (no more event.id, which the old { for: event.id } relied on). Same for now replacing the removed event.ts.

⚠️ Heads-up: the same example still appears verbatim at the top of develop/SKILL.md (lines ~72-77) and was not updated here — it still uses event.ts and event.id. Since this PR is the one removing those fields, that lead example now contradicts the reference. Worth updating in this PR. (Those lines weren't in the diff, so I couldn't anchor an inline comment there — see the review summary.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the SKILL.md lead example in bf5ac94event.ts/event.id gone (records Date.now(), ack carries text), matching the corrected Event<P> this PR introduces.

}

@OnClose
Expand Down Expand Up @@ -78,14 +79,20 @@ type per engagement.

```ts
interface Event<P = unknown> {
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice touch documenting eventId? as "server-assigned (UUID v7); may be absent inbound" — it signals to handlers not to rely on it existing inbound.

Nit: since the old example's { for: event.id } is gone, consider adding a one-liner that handlers needing correlation should carry their own id in the payload (not lean on eventId). Optional.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left as optional — the eventId? doc already signals not to rely on it inbound; correlation-in-payload is a reasonable pattern but out of scope for this doc pass.

}
```

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<UserMessage>`, then read
`event.payload.text`.

Expand Down
80 changes: 80 additions & 0 deletions plugins/cef/skills/develop/references/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -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: <message>"`):

```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: <table>.<col>` | 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: <table>.<col>` | 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/<agentId>`** verifies the built bundle's handlers match the
manifest routing (catches "event handled but nothing happened").
Loading
Loading