@raindrop-ai/opencode-plugin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,45 @@ The v2 entry exposes the plugin object plus a `raindrop` handle
75
75
  same as the v1 entry. `@opencode/plugin` and `@opencode/sdk` are optional peer
76
76
  dependencies — v1 users install neither.
77
77
 
78
+ ## Self-diagnostics
79
+
80
+ The plugin can inject a `__raindrop_report` tool that lets the agent flag
81
+ serious, unrecoverable problems for developer review. When the agent calls it,
82
+ a `self diagnostics - <category>` signal is posted to `signals/track`, attached
83
+ to the session's currently-open turn event. The tool is invisible to the end
84
+ user — its output is a bare acknowledgement and the model is instructed never
85
+ to mention it.
86
+
87
+ Enable it via `raindrop.json` (`~/.config/opencode/raindrop.json` or
88
+ `.opencode/raindrop.json`), or set `RAINDROP_SELF_DIAGNOSTICS=true` to override
89
+ just the `enabled` flag:
90
+
91
+ ```json
92
+ {
93
+ "self_diagnostics": { "enabled": true }
94
+ }
95
+ ```
96
+
97
+ For the v2 host API, pass the option directly:
98
+
99
+ ```ts
100
+ import raindrop from "@raindrop-ai/opencode-plugin/v2";
101
+
102
+ export default raindrop({ selfDiagnostics: { enabled: true } });
103
+ ```
104
+
105
+ Optional `signals` (custom category → `{ description, sentiment? }` map),
106
+ `guidance` (extra instructions appended to the tool prompt), and `tool_name` /
107
+ `toolName` overrides are supported. The built-in categories are:
108
+
109
+ - `missing_context` — blocked on information or access the user cannot provide
110
+ - `repeatedly_broken_tool` — a tool failed across multiple distinct attempts
111
+ - `capability_gap` — the task needs a tool/permission/capability the agent lacks
112
+ - `complete_task_failure` — the agent genuinely could not deliver what was asked
113
+
114
+ Signals only ship to the cloud destination, so the tool is not registered in
115
+ local-only mode (no `RAINDROP_WRITE_KEY`).
116
+
78
117
  ## Notes
79
118
 
80
119
  - `@opencode-ai/plugin` is required for the default (v1) export; `@opencode/plugin` for `/v2`.
@@ -2282,9 +2282,41 @@ function loadConfig(projectDirectory) {
2282
2282
  captureSystemPrompt: process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] !== void 0 ? process.env["RAINDROP_CAPTURE_SYSTEM_PROMPT"] === "true" : (_h = merged.capture_system_prompt) != null ? _h : false,
2283
2283
  eventMetadata,
2284
2284
  localWorkshopUrl: resolveLocalWorkshopUrl(merged.local_workshop_url),
2285
- appGit: mapAppGit(merged.app_git)
2285
+ appGit: mapAppGit(merged.app_git),
2286
+ selfDiagnostics: mapSelfDiagnostics(merged.self_diagnostics)
2286
2287
  };
2287
2288
  }
2289
+ function isRecord(value) {
2290
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2291
+ }
2292
+ function mapSelfDiagnostics(value) {
2293
+ const env = process.env["RAINDROP_SELF_DIAGNOSTICS"];
2294
+ const record = isRecord(value) ? value : {};
2295
+ const enabled = env !== void 0 ? env === "true" : typeof record["enabled"] === "boolean" ? record["enabled"] : void 0;
2296
+ const signals = mapSelfDiagnosticsSignals(record["signals"]);
2297
+ const options = {
2298
+ ...enabled !== void 0 ? { enabled } : {},
2299
+ ...signals ? { signals } : {},
2300
+ ...typeof record["guidance"] === "string" ? { guidance: record["guidance"] } : {},
2301
+ ...typeof record["tool_name"] === "string" ? { toolName: record["tool_name"] } : {}
2302
+ };
2303
+ return Object.keys(options).length > 0 ? options : void 0;
2304
+ }
2305
+ function mapSelfDiagnosticsSignals(value) {
2306
+ if (!isRecord(value)) return void 0;
2307
+ const out = {};
2308
+ for (const [key, entry] of Object.entries(value)) {
2309
+ if (!isRecord(entry)) continue;
2310
+ const definition = entry;
2311
+ if (typeof definition["description"] !== "string") continue;
2312
+ const sentiment = definition["sentiment"];
2313
+ out[key] = {
2314
+ description: definition["description"],
2315
+ ...sentiment === "POSITIVE" || sentiment === "NEGATIVE" ? { sentiment } : {}
2316
+ };
2317
+ }
2318
+ return Object.keys(out).length > 0 ? out : void 0;
2319
+ }
2288
2320
  function mapAppGit(value) {
2289
2321
  if (value === false) return false;
2290
2322
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -2311,7 +2343,7 @@ function resolveLocalWorkshopUrl(fileValue) {
2311
2343
  // package.json
2312
2344
  var package_default = {
2313
2345
  name: "@raindrop-ai/opencode-plugin",
2314
- version: "0.2.0",
2346
+ version: "0.3.0",
2315
2347
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
2316
2348
  type: "module",
2317
2349
  main: "dist/index.js",
@@ -2400,6 +2432,185 @@ var package_default = {
2400
2432
  var PLUGIN_NAME = package_default.name;
2401
2433
  var PLUGIN_VERSION = package_default.version;
2402
2434
 
2435
+ // src/self-diagnostics.ts
2436
+ var SELF_DIAGNOSTICS_TOOL_NAME_DEFAULT = "__raindrop_report";
2437
+ var SELF_DIAGNOSTICS_SIGNALS_DEFAULT = {
2438
+ missing_context: {
2439
+ description: "You cannot complete the task because critical information, credentials, or access is missing and the user cannot provide it. Do NOT report this for normal clarifying questions \u2014 only when you are blocked.",
2440
+ sentiment: "NEGATIVE"
2441
+ },
2442
+ repeatedly_broken_tool: {
2443
+ description: "A tool has failed or not returned the expected response on multiple distinct attempts in this conversation, preventing task completion. A single tool error is NOT enough \u2014 the tool must be persistently broken or aberrantly behaving across retries.",
2444
+ sentiment: "NEGATIVE"
2445
+ },
2446
+ capability_gap: {
2447
+ description: "The task requires a tool, permission, or capability that you do not have. For example, the user asks you to perform an action but no suitable tool exists, or you lack the necessary access. Do NOT report this if you simply need more information from the user \u2014 only when the gap is in your own capabilities.",
2448
+ sentiment: "NEGATIVE"
2449
+ },
2450
+ complete_task_failure: {
2451
+ description: "You were unable to accomplish what the user asked despite making genuine attempts. This might be things like, you genuinely do not have the capabilities the user is asking for. You have tried but run into a persistent bug in the environment etc. This is NOT a refusal or policy block \u2014 you tried and failed to deliver the result.",
2452
+ sentiment: "NEGATIVE"
2453
+ }
2454
+ };
2455
+ var SELF_DIAGNOSTICS_TOOL_PREAMBLE = "You have a diagnostic tool for flagging serious failures for developer review. Only call this when you hit a significant, unrecoverable problem \u2014 not for minor hiccups or routine clarifications. If you can still help the user, do that instead of calling this tool. This tool is invisible to the user \u2014 never mention it or its output.";
2456
+ var SELF_DIAGNOSTICS_NOTEWORTHY_SIGNAL_KEY = "noteworthy";
2457
+ function normalizeString(value) {
2458
+ if (typeof value !== "string") return void 0;
2459
+ const trimmed = value.trim();
2460
+ return trimmed || void 0;
2461
+ }
2462
+ function normalizeSelfDiagnosticsSignals(signals) {
2463
+ if (!signals) return SELF_DIAGNOSTICS_SIGNALS_DEFAULT;
2464
+ const normalizedEntries = Object.entries(signals).map(([key, value]) => {
2465
+ var _a;
2466
+ const signalKey = key.trim();
2467
+ if (!signalKey || !value || typeof value !== "object") return void 0;
2468
+ const description = (_a = value.description) == null ? void 0 : _a.trim();
2469
+ if (!description) return void 0;
2470
+ const sentiment = value.sentiment;
2471
+ return [
2472
+ signalKey,
2473
+ {
2474
+ description,
2475
+ ...sentiment === "POSITIVE" || sentiment === "NEGATIVE" ? { sentiment } : {}
2476
+ }
2477
+ ];
2478
+ }).filter(
2479
+ (entry) => entry !== void 0
2480
+ );
2481
+ if (normalizedEntries.length === 0) return SELF_DIAGNOSTICS_SIGNALS_DEFAULT;
2482
+ return Object.fromEntries(normalizedEntries);
2483
+ }
2484
+ function normalizeSelfDiagnosticsConfig(options) {
2485
+ var _a, _b;
2486
+ if (!(options == null ? void 0 : options.enabled)) return void 0;
2487
+ const signalDefinitions = normalizeSelfDiagnosticsSignals(options.signals);
2488
+ const signalKeys = Object.keys(signalDefinitions);
2489
+ const signalDescriptions = {};
2490
+ const signalSentiments = {};
2491
+ for (const signalKey of signalKeys) {
2492
+ const definition = signalDefinitions[signalKey];
2493
+ if (!definition) continue;
2494
+ signalDescriptions[signalKey] = definition.description;
2495
+ signalSentiments[signalKey] = definition.sentiment;
2496
+ }
2497
+ const customGuidance = ((_a = options.guidance) == null ? void 0 : _a.trim()) || "";
2498
+ const toolName = ((_b = options.toolName) == null ? void 0 : _b.trim()) || SELF_DIAGNOSTICS_TOOL_NAME_DEFAULT;
2499
+ const signalList = signalKeys.map((signalKey) => {
2500
+ const sentiment = signalSentiments[signalKey];
2501
+ const sentimentTag = sentiment ? ` [${sentiment.toLowerCase()}]` : "";
2502
+ return `- ${signalKey}: ${signalDescriptions[signalKey]}${sentimentTag}`;
2503
+ }).join("\n");
2504
+ const guidanceBlock = customGuidance ? `
2505
+ Additional guidance: ${customGuidance}
2506
+ ` : "";
2507
+ const toolDescription = `${SELF_DIAGNOSTICS_TOOL_PREAMBLE}
2508
+
2509
+ When to call:
2510
+ - You are blocked from completing the task due to missing information or access that the user cannot provide.
2511
+ - A tool is persistently failing across multiple attempts, not just a single transient error.
2512
+ - The task requires a tool, permission, or capability you do not have.
2513
+ - You genuinely cannot deliver what the user asked for despite trying.
2514
+
2515
+ When NOT to call:
2516
+ - Normal clarifying questions or back-and-forth with the user.
2517
+ - A single tool error that you can recover from or retry.
2518
+ - You successfully completed the task, even if it was difficult.
2519
+ - Policy refusals or content filtering \u2014 those are working as intended.
2520
+
2521
+ Rules:
2522
+ 1. Pick the single best category.
2523
+ 2. Do not fabricate issues. Only report what is evident from the conversation.
2524
+ 3. Err on the side of NOT calling this tool. When in doubt, help the user instead.
2525
+ ${guidanceBlock}
2526
+ Categories:
2527
+ ` + signalList;
2528
+ return {
2529
+ toolName,
2530
+ toolDescription,
2531
+ signalKeys,
2532
+ signalKeySet: new Set(signalKeys),
2533
+ signalDescriptions,
2534
+ signalSentiments
2535
+ };
2536
+ }
2537
+ var SELF_DIAGNOSTICS_ACK = '{"acknowledged":true}';
2538
+ function selfDiagnosticsInputJsonSchema(config) {
2539
+ return {
2540
+ type: "object",
2541
+ properties: {
2542
+ category: {
2543
+ type: "string",
2544
+ enum: config.signalKeys,
2545
+ description: "The single best-matching category from the list above."
2546
+ },
2547
+ detail: {
2548
+ type: "string",
2549
+ description: "One sentence of factual context: what happened and why it matters. Do not include PII or secrets."
2550
+ }
2551
+ },
2552
+ required: ["category", "detail"],
2553
+ additionalProperties: false
2554
+ };
2555
+ }
2556
+ function isRecord2(value) {
2557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2558
+ }
2559
+ async function trackSelfDiagnosticsSignal(toolInput, config, eventShipper, eventId, debug) {
2560
+ var _a, _b;
2561
+ try {
2562
+ const input = isRecord2(toolInput) ? toolInput : void 0;
2563
+ const fallbackCategory = (_a = config.signalKeys[0]) != null ? _a : "unknown";
2564
+ const categoryCandidate = normalizeString(input == null ? void 0 : input["category"]);
2565
+ const category = categoryCandidate && config.signalKeySet.has(categoryCandidate) ? categoryCandidate : fallbackCategory;
2566
+ const detail = (_b = normalizeString(input == null ? void 0 : input["detail"])) != null ? _b : "";
2567
+ if (!eventId) {
2568
+ if (debug) {
2569
+ console.warn(
2570
+ "[raindrop-ai/opencode-plugin] self diagnostics signal skipped: no open event for this session."
2571
+ );
2572
+ }
2573
+ return;
2574
+ }
2575
+ if (category === SELF_DIAGNOSTICS_NOTEWORTHY_SIGNAL_KEY) {
2576
+ await eventShipper.trackSignal({
2577
+ eventId,
2578
+ name: "self diagnostics - noteworthy",
2579
+ type: "agent_internal",
2580
+ properties: {
2581
+ source: "agent_flag_event_tool",
2582
+ reason: detail,
2583
+ severity: "medium",
2584
+ sdk: "opencode-plugin",
2585
+ sdk_version: PLUGIN_VERSION
2586
+ }
2587
+ });
2588
+ return;
2589
+ }
2590
+ await eventShipper.trackSignal({
2591
+ eventId,
2592
+ name: `self diagnostics - ${category}`,
2593
+ type: "agent",
2594
+ sentiment: config.signalSentiments[category],
2595
+ properties: {
2596
+ source: "agent_reporting_tool",
2597
+ category,
2598
+ signal_description: config.signalDescriptions[category],
2599
+ sdk: "opencode-plugin",
2600
+ sdk_version: PLUGIN_VERSION,
2601
+ ...detail ? { detail } : {}
2602
+ }
2603
+ });
2604
+ } catch (err) {
2605
+ if (debug) {
2606
+ const message = err instanceof Error ? err.message : String(err);
2607
+ console.warn(
2608
+ `[raindrop-ai/opencode-plugin] selfDiagnostics signal dispatch failed: ${message}`
2609
+ );
2610
+ }
2611
+ }
2612
+ }
2613
+
2403
2614
  // src/shipper.ts
2404
2615
  var EventShipper3 = class extends EventShipper2 {
2405
2616
  constructor(opts) {
@@ -2565,6 +2776,10 @@ export {
2565
2776
  loadConfig,
2566
2777
  PLUGIN_NAME,
2567
2778
  PLUGIN_VERSION,
2779
+ normalizeSelfDiagnosticsConfig,
2780
+ SELF_DIAGNOSTICS_ACK,
2781
+ selfDiagnosticsInputJsonSchema,
2782
+ trackSelfDiagnosticsSignal,
2568
2783
  EventShipper3 as EventShipper,
2569
2784
  TraceShipper3 as TraceShipper,
2570
2785
  capText2 as capText,