Skip to main content

build-w2a-sensor

Use this skill when the user asks to "build a W2A sensor", "create a world2agent sensor", "add a sensor for <source>", or otherwise wants to emit signals into the World2Agent protocol from a new data source. Guides discovery → design → scaffold → install recipe → test.

Quellinformationen

Repository
machinepulse-ai/world2agent
Letzte Quellaktivität
9. Mai 2026 um 12:39
Erkannte Sprache von SKILL.md
Englisch
Sterne
1.131
Forks
41

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
build-w2a-sensor
description
Use this skill when the user asks to "build a W2A sensor", "create a world2agent sensor", "add a sensor for <source>", or otherwise wants to emit signals into the World2Agent protocol from a new data source. Guides discovery → design → scaffold → install recipe → test.
version
0.1.0
user-invocable
true
disable-model-invocation
false
# Build a World2Agent Sensor You are guiding a developer through designing and building a W2A sensor — a package that subscribes to an external source and emits `W2ASignal` per the World2Agent protocol. **Protocol reference:** https://github.com/machinepulse-ai/world2agent — the schema under `schema/0.1/` is authoritative for every field shape. If anything in this skill contradicts the schema, the schema wins. Your first job is **discovery, not code**. Sensors that skip discovery ship either too much config (tuning knobs no one touches) or too little signal quality (vague summaries, wrong event types, missing install recipe). Both are painful to fix later. A sensor package has two deliverables: 1. **The runtime** (`src/`) — connects to the source, transforms incoming events into `W2ASignal`, calls `ctx.emit()`. 2. **`SETUP.md`** — the install recipe. When the consumer agent runs the host's sensor-add flow (e.g. `/world2agent:sensor-add <package>` in Claude Code), it reads this file, runs the Q&A defined inside, writes the sensor's entry into `~/.world2agent/config.json`, and writes a per-user handler skill into the agent runtime's skills directory. The handler is generated by the install flow, not shipped in the package. Do not scaffold until Phase 1 and Phase 2 are settled. --- ## Phase 1 — Interrogate the source Ask these conversationally (batch in one message, don't serialize). Adapt to what the user already said. ### 1. What does the sensor connect to? Name the source platform and give the user-facing "why this matters". This drives `source_type`, package naming, and what domain namespace the event types use. ### 2. How do events reach the sensor? Pick the highest available option — **do not default to polling if a push channel exists**. | Mechanism | Use when | Notes | |---|---|---| | **Official push** (webhook / SSE / WebSocket) | The source offers it | Preferred: seconds latency, no wasted calls, no cursor drift | | **Official API polling** | No push offered | Persist a cursor via `ctx.store` | | **Schedule-triggered (cron)** | Source is inherently periodic (digests, daily summaries) | Time itself is the trigger | | **File-system watch / scraping** | No official API at all | Last resort — brittle and hard to test | ### 3. What event types will it emit? List every triple it will emit as `domain.entity.action` (open namespace — you coin them). Examples: `messaging.message.received`, `repo.issue.created`, `market.quote.threshold_crossed`. These strings are your sensor's public contract — consumers pattern-match against them, so stability matters. `domain` is the **abstract source space** (`messaging`, `repo`, `market`, `calendar`), **not the platform name**. The platform identity already lives in `source.source_type` (`"slack"`, `"github"`, `"jira"`). A Slack sensor emits `messaging.message.mentioned`, not `slack.message.mentioned` — that's what lets a consumer write `handler.on("messaging.message.mentioned")` once and match @-events from Slack, Discord, Lark, and Teams alike. `action` is a verb in past tense (`mentioned`, `opened`, `received`, `entered`), not a base form or gerund (`mention` ❌, `opening` ❌). ### 4. What config does the sensor need? Credentials (if any) + the smallest number of behavioural fields to make end-to-end work. Defaults beat required fields. Five is already a lot. Save tuning knobs (`max_retries`, `timeout_ms`, `batch_size`) for when a real user asks. If the user can't answer #3 crisply, stop and work on that — vague event taxonomies produce vague summaries, and `event.summary` is what agents read first. --- ## Phase 2 — Design the signal For each event type, pin down the signal shape before writing code. ### `event.summary` The summary is the soul of the signal — an agent reading only this line must be able to decide whether and how to act. Use the pattern `[Actor] [Action] [Object] in [Context]; [Impact]`. Sketch one summary template per event type: > `repo.pull_request.opened` → *"{author} opened PR #{num} in {repo}: {title}; touches {files_count} files across {areas}, needs review before {deadline}."* Bad summaries: "new message", "PR update", "price moved". Reject and rewrite. ### `event.type` vs `source_event` vs `attachments` Three channels, three jobs. Keep them separate: | Field | Carries | Example | |---|---|---| | `event` | Normalized cross-source classification — `type`, `occurred_at`, `summary` | `type: "messaging.message.mentioned"`, summary text | | `source_event` | Self-describing structured data from the source: `{ schema, data }` with JSON Schema draft-07 for `schema` | IDs, numbers, booleans, enums the graph/agent will reason over | | `attachments` | Unstructured content blobs (message bodies, diffs, images, audio) | Text body of the message, PDF file, screenshot | Every property in `source_event.schema` SHOULD carry a `description` — that's what makes the payload self-describing. A schema that only declares types (`{ "type": "integer" }`) leaves the consumer guessing what the value means; with a description (`{ "type": "integer", "description": "Stars gained in the last 24 hours" }`) an agent can reason about the data without sensor-specific knowledge. Never put structured machine data in an attachment. Never put large blobs in `source_event.data`. ### Attachment choice: inline vs reference `Attachment` is a tagged union: - `{ type: "inline", mime_type, description, data }` — content embedded as a string (UTF-8 for text, base64 for binary). - `{ type: "reference", mime_type, description, uri }` — external pointer, consumers fetch on demand. Prefer `reference` for anything larger than a few KiB, already-addressable URLs, or content the consumer may not need. `description` is required on both and must be AI-readable — it's how a consumer decides whether to open the attachment. --- ## Phase 3 — Scaffold the package Layout: ``` <package-dir>/ ├── package.json ├── tsconfig.json ├── SETUP.md — install/Q&A recipe (Phase 4) ├── README.md — one-pager for package registries └── src/ ├── index.ts — defineSensor + re-exports ├── start.ts — main loop (pattern from Phase 1 #2) ├── transform.ts — source event → W2ASignal ├── types.ts — config schema + source types └── bin.ts — CLI entry point ``` ### `package.json` Substitute `<package-name>` and `<bin-name>` with whatever coordinates you publish under. The `keywords` array is the discoverability contract — fill in the real `source_type` decided in Phase 1, do not leave the placeholder. `npm search w2a-sensor` and SensorHub indexing both rely on these keywords. ```json { "name": "<package-name>", "version": "0.1.0", "description": "W2A sensor — <one-line description>", "keywords": [ "world2agent", "w2a", "w2a-sensor", "sensor", "agent", "<source_type>" ], "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "bin": { "<bin-name>": "./dist/bin.js" }, "scripts": { "start": "node dist/bin.js", "build": "tsc --build", "clean": "rm -rf dist *.tsbuildinfo", "postpublish": "npx -y @world2agent/notify-hub || true" }, "dependencies": { "@world2agent/sdk": "^0.1.0", "zod": "^3.25.0" }, "devDependencies": { "@types/node": "^25.5.0", "typescript": "^5.8.0" }, "files": ["dist", "SETUP.md", "README.md"], "w2a": { "type": "sensor", "source_type": "<source_type>", "signals": ["<domain>.<entity>.<action>"], "setup": "./SETUP.md" } } ``` The first five keywords are mandatory for every W2A sensor — they are how consumers discover sensors via `npm search`. The last entry is the `source_type` decided in Phase 1 (e.g. `"github"`, `"hackernews"`, `"slack"`). Add additional source-specific tags (`"trending"`, `"oauth"`, etc.) only if they would actually help discovery. The `w2a` block is tooling metadata the install CLI reads; it is not part of the wire protocol. ### `src/index.ts` ```typescript import { defineSensor } from "@world2agent/sdk/sensor"; import { <name>ConfigSchema } from "./types.js"; import { start } from "./start.js"; export default defineSensor({ id: "<package-name>", version: "0.1.0", source_type: "<source_type>", configSchema: <name>ConfigSchema, // SDK / install-tooling hint — NOT part of the wire protocol. // `{ type: "none" }` when no credentials; `{ type: "api_key", fields: [...] }` // with one entry per credential prompt. See @world2agent/sdk for shape. auth: { type: "none" }, start, }); export { transform<Name>Event } from "./transform.js"; export { <name>ConfigSchema } from "./types.js"; export type { <Name>Config } from "./types.js"; ``` ### `src/transform.ts` `createSignal()` auto-injects `source.package` from the meta you pass — never set it manually. ```typescript import { createSignal, type W2ASignal } from "@world2agent/sdk"; const SENSOR_META = { id: "<package-name>", version: "0.1.0", source_type: "<source_type>", } as const; export function transform<Name>Event(/* source event args */): W2ASignal { return createSignal(SENSOR_META, { source: { user_identity: "..." }, event: { type: "<domain>.<entity>.<action>", summary: "...", // Phase 2 template }, source_event: { schema: { /* JSON Schema draft-07 describing `data`. Every property SHOULD carry a `description` so the payload is self-describing without sensor-specific knowledge. */ }, data: { /* original event fields */ }, }, attachments: [ { type: "inline", // or "reference" mime_type: "text/plain", description: "...", // required; AI-readable data: "...", // or `uri: "..."` for type: "reference" }, ], }); } ``` ### `src/start.ts` — pick the pattern from Phase 1 #2 The sections below are skeletons. Fill in the source-specific parts. **Webhook (push)** — serve an HTTP endpoint, verify signature, transform, emit. ```typescript import { createServer } from "node:http"; import { createHmac, timingSafeEqual } from "node:crypto"; import type { SensorContext, CleanupFn } from "@world2agent/sdk"; import { transform<Name>Event } from "./transform.js"; import type { <Name>Config } from "./types.js"; export async function start(ctx: SensorContext<<Name>Config>): Promise<CleanupFn> { const server = createServer(async (req, res) => { if (req.method !== "POST" || req.url !== ctx.config.path) { res.writeHead(404).end(); return; } // 1. Read body. 2. Verify HMAC via timingSafeEqual. // 3. JSON.parse. 4. transform<Name>Event → ctx.emit(signal). // 5. res.writeHead(200).end(). }); await new Promise<void>(r => server.listen(ctx.config.port, r)); ctx.reportHealth("ok"); return () => new Promise<void>((res, rej) => server.close(err => err ? rej(err) : res())); } ``` **WebSocket (push)** — connect, auto-reconnect on close, emit per message. ```typescript export async function start(ctx: SensorContext<<Name>Config>): Promise<CleanupFn> { let reconnect = true; let ws: WebSocket | undefined; function connect() { ws = new WebSocket(ctx.config.ws_url); ws.addEventListener("open", () => ctx.reportHealth("ok")); ws.addEventListener("message", async ev => { await ctx.emit(transform<Name>Event(JSON.parse(String(ev.data)))); }); ws.addEventListener("close", () => { ctx.reportHealth("degraded", "disconnected"); if (reconnect) setTimeout(connect, 5000); }); } connect(); return () => { reconnect = false; ws?.close(); }; } ``` **Polling (fallback)** — `@world2agent/sdk/helpers` ships `createPollLoop`; persist cursor via `ctx.store`. ```typescript import { createPollLoop } from "@world2agent/sdk/helpers"; export async function start(ctx: SensorContext<<Name>Config>): Promise<CleanupFn> { const { cleanup } = createPollLoop({
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen