opencode-superagent 0.1.0 → 0.2.1

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.
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Plugin version, read from the package's own `package.json`.
5
+ *
6
+ * The version is the only thing this module exposes: it is deliberately not a
7
+ * diagnostics dump, so logging it can never leak config, tokens, absolute
8
+ * paths or session ids.
9
+ */
10
+ /** Shown when no candidate `package.json` can be read or parsed. */
11
+ export const UNKNOWN_VERSION = "0.0.0-unknown";
12
+ /**
13
+ * `package.json` locations relative to this module, most specific first:
14
+ * - source (dev / tests): `src/version.ts` → `../package.json`
15
+ * - build output: `dist/src/version.js` → `../../package.json`
16
+ *
17
+ * Both are tried so the same code works in the repo and in a packed tarball.
18
+ */
19
+ const CANDIDATES = ["../package.json", "../../package.json"];
20
+ /** Extract `version` from raw `package.json` text; `undefined` when unusable. */
21
+ export function versionFromPackageJson(raw) {
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ if (typeof parsed !== "object" || parsed === null)
30
+ return undefined;
31
+ const value = parsed.version;
32
+ if (typeof value !== "string")
33
+ return undefined;
34
+ const trimmed = value.trim();
35
+ return trimmed === "" ? undefined : trimmed;
36
+ }
37
+ function readRelative(file) {
38
+ try {
39
+ return readFileSync(path.resolve(import.meta.dir, file), "utf8");
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ /**
46
+ * Read the first usable candidate, falling back to {@link UNKNOWN_VERSION}.
47
+ *
48
+ * `read` is injectable so the fallback path is testable without touching the
49
+ * filesystem: pass `() => undefined` for a missing package, or a stub that
50
+ * returns unparsable JSON.
51
+ */
52
+ export function loadVersion(read = readRelative) {
53
+ for (const candidate of CANDIDATES) {
54
+ const raw = read(candidate);
55
+ if (raw === undefined)
56
+ continue;
57
+ const version = versionFromPackageJson(raw);
58
+ if (version !== undefined)
59
+ return version;
60
+ }
61
+ return UNKNOWN_VERSION;
62
+ }
63
+ let cached;
64
+ /** Cached plugin version; the process never changes version at runtime. */
65
+ export function pluginVersion() {
66
+ cached ??= loadVersion();
67
+ return cached;
68
+ }
69
+ /**
70
+ * One-line startup notice. Says what is loaded and that upgrading an already
71
+ * running gateway is not enough — the plugin has no hot reload, so a service
72
+ * restart is required before the new code is active.
73
+ */
74
+ export function versionNotice() {
75
+ return `superagent ${pluginVersion()} loaded; restart the OpenCode gateway after upgrading (no hot reload)`;
76
+ }
@@ -4,13 +4,20 @@ import { Effect } from "effect";
4
4
  export declare const WATCHDOG_INTERVAL_MS = 60000;
5
5
  /** Default stall threshold. Overridable with the `stallTimeoutMs` plugin option. */
6
6
  export declare const STALL_TIMEOUT_MS: number;
7
- /** Prefix marking a worker already reported as stalled. Guards duplicate watchdog alerts. */
7
+ /**
8
+ * Older records may still carry the watchdog marker in `lastReport`; keep
9
+ * honouring it so a restart cannot re-alert the same legacy stall.
10
+ */
8
11
  export declare const STALLED_PREFIX = "watchdog: stalled";
12
+ /** Test/hydrate hook: forget every in-memory stall alert. */
13
+ export declare function resetWatchdogAlerts(): void;
14
+ export declare const checkWorker: (ctx: Plugin.Context, sessionID: string, now: number, stallTimeoutMs: number) => Effect.Effect<void, never, never>;
9
15
  /**
10
16
  * Periodic stall detector. Every interval it flags `running` workers whose
11
17
  * session has been busy longer than the stall timeout without an `idle`
12
- * update, reports them to the router, and marks them `failed`. It never
13
- * interrupts: stalled workers stay alive until someone stops or re-dispatches
14
- * them. All errors are swallowed with a warning so the loop never dies.
18
+ * update, and reports them to the router. It is alert-only: the worker record
19
+ * keeps its `running` state, its ticket stays open and the worker is never
20
+ * interrupted, so the Router stays in control. Alerts are deduped per busy
21
+ * window. All errors are swallowed with a warning so the loop never dies.
15
22
  */
16
23
  export declare const registerWatchdog: (ctx: Plugin.Context) => Effect.Effect<void, never, import("effect/Scope").Scope>;
@@ -1,24 +1,37 @@
1
1
  import { DateTime, Effect, Option, Stream } from "effect";
2
2
  import { ensureRouter } from "./router.js";
3
- import { saveWorker, store, toSessionID } from "./state.js";
4
- import { closeTicketsForWorker } from "./ticket.js";
3
+ import { store, toSessionID } from "./state.js";
5
4
  /** Default interval between stall checks. Overridable with the `watchdogIntervalMs` plugin option. */
6
5
  export const WATCHDOG_INTERVAL_MS = 60_000;
7
6
  /** Default stall threshold. Overridable with the `stallTimeoutMs` plugin option. */
8
7
  export const STALL_TIMEOUT_MS = 30 * 60_000;
9
- /** Prefix marking a worker already reported as stalled. Guards duplicate watchdog alerts. */
8
+ /**
9
+ * Older records may still carry the watchdog marker in `lastReport`; keep
10
+ * honouring it so a restart cannot re-alert the same legacy stall.
11
+ */
10
12
  export const STALLED_PREFIX = "watchdog: stalled";
13
+ /**
14
+ * Sessions already reported for their current busy window. Alert-only mode
15
+ * never mutates the worker record, so dedupe lives in memory. Keying by
16
+ * `time.busy` lets a resumed worker alert once more if it stalls again.
17
+ */
18
+ const alerted = new Map();
19
+ /** Test/hydrate hook: forget every in-memory stall alert. */
20
+ export function resetWatchdogAlerts() {
21
+ alerted.clear();
22
+ }
11
23
  function optionMs(ctx, key, fallback) {
12
24
  const raw = ctx.options[key];
13
25
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
14
26
  return fallback;
15
27
  return raw;
16
28
  }
17
- const checkWorker = Effect.fn("superagent.watchdogCheck")(function* (ctx, sessionID, now, stallTimeoutMs) {
29
+ export const checkWorker = Effect.fn("superagent.watchdogCheck")(function* (ctx, sessionID, now, stallTimeoutMs) {
18
30
  const worker = store.workers.get(sessionID);
19
- if (worker === undefined || worker.state !== "running")
31
+ if (worker === undefined || worker.state !== "running") {
32
+ alerted.delete(sessionID);
20
33
  return;
21
- // Already alerted: only the running→failed transition reports once.
34
+ }
22
35
  if (worker.lastReport !== undefined && worker.lastReport.startsWith(STALLED_PREFIX))
23
36
  return;
24
37
  const info = yield* ctx.session.get({ sessionID: toSessionID(sessionID) }).pipe(Effect.option);
@@ -27,13 +40,20 @@ const checkWorker = Effect.fn("superagent.watchdogCheck")(function* (ctx, sessio
27
40
  return;
28
41
  }
29
42
  const busy = info.value.time.busy;
30
- if (busy === undefined)
43
+ if (busy === undefined) {
44
+ alerted.delete(sessionID);
31
45
  return;
46
+ }
32
47
  const busyMs = DateTime.toEpochMillis(busy);
33
48
  const idle = info.value.time.idle;
34
49
  // While a session runs, `idle` still holds the previous execution's end.
35
50
  // Only an `idle` newer than `busy` proves progress since this execution started.
36
- if (idle !== undefined && DateTime.toEpochMillis(idle) > busyMs)
51
+ if (idle !== undefined && DateTime.toEpochMillis(idle) > busyMs) {
52
+ alerted.delete(sessionID);
53
+ return;
54
+ }
55
+ // One alert per busy window: duplicate checks must not spam the Router.
56
+ if (alerted.get(sessionID) === busyMs)
37
57
  return;
38
58
  const stalledMs = now - busyMs;
39
59
  if (stalledMs <= stallTimeoutMs)
@@ -47,15 +67,11 @@ const checkWorker = Effect.fn("superagent.watchdogCheck")(function* (ctx, sessio
47
67
  .synthetic({ sessionID: toSessionID(routerID), text, description: "superagent watchdog", delivery: "queue" })
48
68
  .pipe(Effect.catch((cause) => Effect.logWarning("superagent watchdog failed to report to router", { cause })));
49
69
  }
50
- // Alert only: never interrupt automatically, so long tasks are not killed by mistake.
51
- worker.state = "failed";
52
- worker.lastReport = `${STALLED_PREFIX} ${minutes}m${worker.lastReport === undefined ? "" : ` | ${worker.lastReport}`}`;
53
- yield* saveWorker(ctx, worker);
54
- yield* closeTicketsForWorker(ctx, {
55
- sessionID,
56
- outcome: "failed",
57
- summary: `watchdog: stalled ${minutes}m`,
58
- }).pipe(Effect.catch((cause) => Effect.logWarning("superagent watchdog failed to close ticket", { sessionID, cause })));
70
+ alerted.set(sessionID, busyMs);
71
+ // Alert only: never change worker.state, never close its ticket and never
72
+ // interrupt automatically, so a long task is not killed by mistake. The
73
+ // Router decides whether to stop/re-dispatch.
74
+ yield* Effect.logWarning("superagent watchdog: worker stalled", { sessionID, minutes });
59
75
  });
60
76
  const checkAll = Effect.fn("superagent.watchdogCheckAll")(function* (ctx, stallTimeoutMs) {
61
77
  const now = Date.now();
@@ -66,9 +82,10 @@ const checkAll = Effect.fn("superagent.watchdogCheckAll")(function* (ctx, stallT
66
82
  /**
67
83
  * Periodic stall detector. Every interval it flags `running` workers whose
68
84
  * session has been busy longer than the stall timeout without an `idle`
69
- * update, reports them to the router, and marks them `failed`. It never
70
- * interrupts: stalled workers stay alive until someone stops or re-dispatches
71
- * them. All errors are swallowed with a warning so the loop never dies.
85
+ * update, and reports them to the router. It is alert-only: the worker record
86
+ * keeps its `running` state, its ticket stays open and the worker is never
87
+ * interrupted, so the Router stays in control. Alerts are deduped per busy
88
+ * window. All errors are swallowed with a warning so the loop never dies.
72
89
  */
73
90
  export const registerWatchdog = Effect.fn("superagent.registerWatchdog")(function* (ctx) {
74
91
  const intervalMs = optionMs(ctx, "watchdogIntervalMs", WATCHDOG_INTERVAL_MS);
@@ -8,4 +8,10 @@ import { type WorkerRecord } from "./state.js";
8
8
  * between the two writes can only lose the summary, never the evidence.
9
9
  */
10
10
  export declare const captureReport: (ctx: Plugin.Context, worker: WorkerRecord, text: string) => Effect.Effect<import("./report.js").ReportRecord, never, never>;
11
+ /**
12
+ * Apply a `session.idle` notification. Only a running worker may become idle:
13
+ * `done`, `failed` and `aborted` are terminal for the lifecycle, and an idle
14
+ * event must never resurrect or overwrite them.
15
+ */
16
+ export declare const handleIdle: (ctx: Plugin.Context, sessionID: string) => Effect.Effect<void, never, never>;
11
17
  export declare const registerWatcher: (ctx: Plugin.Context) => Effect.Effect<void, never, import("effect/Scope").Scope>;
@@ -1,8 +1,9 @@
1
1
  import { Effect, Stream } from "effect";
2
- import { buildReport, reportNotice, reportSummary, saveReport } from "./report.js";
2
+ import { buildReport, lastAssistantText, reportNotice, reportSummary, saveReport } from "./report.js";
3
3
  import { ensureRouter } from "./router.js";
4
4
  import { saveWorker, store, toSessionID } from "./state.js";
5
5
  import { closeTicketsForWorker } from "./ticket.js";
6
+ import { closeOutcomeForWorkerState } from "./ticket-state.js";
6
7
  /**
7
8
  * Capture one finished report: the complete text goes to `report:<sessionID>`
8
9
  * first, then the worker record keeps a short summary for status lines and the
@@ -15,30 +16,6 @@ export const captureReport = Effect.fn("superagent.captureReport")(function* (ct
15
16
  worker.lastReport = reportSummary(record);
16
17
  return record;
17
18
  });
18
- function lastAssistantText(messages) {
19
- for (let index = messages.length - 1; index >= 0; index--) {
20
- const message = messages[index];
21
- if (typeof message !== "object" || message === null)
22
- continue;
23
- const typed = message;
24
- if (typed.type !== "assistant")
25
- continue;
26
- if (!Array.isArray(typed.content))
27
- continue;
28
- const text = typed.content
29
- .flatMap((part) => {
30
- if (typeof part !== "object" || part === null)
31
- return [];
32
- const candidate = part;
33
- return candidate.type === "text" && typeof candidate.text === "string" ? [candidate.text] : [];
34
- })
35
- .join("\n")
36
- .trim();
37
- if (text !== "")
38
- return text;
39
- }
40
- return undefined;
41
- }
42
19
  function errorDetail(data) {
43
20
  if (typeof data !== "object" || data === null)
44
21
  return undefined;
@@ -66,6 +43,18 @@ function interruptDetail(data) {
66
43
  const reason = data.reason;
67
44
  return typeof reason === "string" && reason !== "" ? `interrupted (${reason})` : "interrupted";
68
45
  }
46
+ /**
47
+ * Apply a `session.idle` notification. Only a running worker may become idle:
48
+ * `done`, `failed` and `aborted` are terminal for the lifecycle, and an idle
49
+ * event must never resurrect or overwrite them.
50
+ */
51
+ export const handleIdle = Effect.fn("superagent.workerIdle")(function* (ctx, sessionID) {
52
+ const worker = store.workers.get(sessionID);
53
+ if (worker === undefined || worker.state !== "running")
54
+ return;
55
+ worker.state = "idle";
56
+ yield* saveWorker(ctx, worker);
57
+ });
69
58
  const handleEnded = Effect.fn("superagent.workerEnded")(function* (ctx, sessionID, state, detail) {
70
59
  const sessions = ctx.session;
71
60
  const worker = store.workers.get(sessionID);
@@ -87,11 +76,12 @@ const handleEnded = Effect.fn("superagent.workerEnded")(function* (ctx, sessionI
87
76
  // A worker that reached a terminal state closes its in-progress ticket.
88
77
  // Interruptions (`idle`) leave it open: the abort tool closes it explicitly
89
78
  // and a plain interrupt may still be resumed with a follow-up.
90
- if (state === "done" || state === "failed") {
79
+ const closeOutcome = closeOutcomeForWorkerState(state);
80
+ if (closeOutcome !== undefined) {
91
81
  yield* closeTicketsForWorker(ctx, {
92
82
  sessionID,
93
- outcome: state,
94
- summary: worker.lastReport ?? report ?? detail ?? `${state} (worker ${sessionID})`,
83
+ outcome: closeOutcome,
84
+ summary: worker.lastReport ?? report ?? detail ?? `${closeOutcome} (worker ${sessionID})`,
95
85
  }).pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to close ticket for ended worker", { sessionID, cause })));
96
86
  }
97
87
  // Keep the thread subscriptions after the worker ends: follow-ups on the
@@ -167,11 +157,7 @@ export const registerWatcher = Effect.fn("superagent.registerWatcher")(function*
167
157
  return;
168
158
  }
169
159
  case "session.idle": {
170
- const worker = store.workers.get(sessionID);
171
- if (worker === undefined || worker.state === "idle" || worker.state === "aborted")
172
- return;
173
- worker.state = "idle";
174
- yield* saveWorker(ctx, worker);
160
+ yield* handleIdle(ctx, sessionID);
175
161
  return;
176
162
  }
177
163
  default:
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Worker records and the in-memory worker registry they live in.
3
+ *
4
+ * This module is intentionally dependency-free: `state.ts` owns KV persistence
5
+ * and the rest of the store, while the ticket layer only needs to look up a
6
+ * worker's state when it gates a dispatch. Keeping the record type and the
7
+ * registry map here lets `ticket.ts` avoid importing `state.ts`, which is what
8
+ * breaks the state ↔ ticket import cycle. `store.workers` in `state.ts` is the
9
+ * same map instance, so both layers always see one registry.
10
+ */
11
+ export type WorkerState = "running" | "idle" | "failed" | "done" | "aborted";
12
+ export declare const WORKER_STATES: ReadonlyArray<WorkerState>;
13
+ export interface WorkerRecord {
14
+ readonly sessionID: string;
15
+ readonly title?: string;
16
+ /**
17
+ * Primary thread this worker was dispatched for. Legacy records decode to
18
+ * `""` and `hydrate` backfills it from `thread.workers`, so every live record
19
+ * carries a forward link to its thread without losing the multi-thread
20
+ * `threads` list.
21
+ */
22
+ threadID: string;
23
+ /** Trigger that authorized the dispatch, when the record knows it. */
24
+ triggerID?: string;
25
+ readonly threads: Array<string>;
26
+ state: WorkerState;
27
+ readonly createdAt: number;
28
+ /** Last persisted write. Refreshed by `saveWorker`, so an aborted record keeps a traceable timestamp. */
29
+ updatedAt?: number;
30
+ lastReport?: string;
31
+ directory?: string;
32
+ }
33
+ /** Canonical in-memory worker registry; `store.workers` points at this map. */
34
+ export declare const workers: Map<string, WorkerRecord>;
35
+ /**
36
+ * Worker states after which the ticket may be dispatched again. `idle` is not
37
+ * here on purpose: an idle session can still be resumed, so a re-dispatch
38
+ * would race a live worker.
39
+ */
40
+ export declare const TERMINAL_WORKER_STATES: ReadonlyArray<WorkerState>;
41
+ export declare function isWorkerTerminal(state: WorkerState): boolean;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Worker records and the in-memory worker registry they live in.
3
+ *
4
+ * This module is intentionally dependency-free: `state.ts` owns KV persistence
5
+ * and the rest of the store, while the ticket layer only needs to look up a
6
+ * worker's state when it gates a dispatch. Keeping the record type and the
7
+ * registry map here lets `ticket.ts` avoid importing `state.ts`, which is what
8
+ * breaks the state ↔ ticket import cycle. `store.workers` in `state.ts` is the
9
+ * same map instance, so both layers always see one registry.
10
+ */
11
+ export const WORKER_STATES = ["running", "idle", "failed", "done", "aborted"];
12
+ /** Canonical in-memory worker registry; `store.workers` points at this map. */
13
+ export const workers = new Map();
14
+ /**
15
+ * Worker states after which the ticket may be dispatched again. `idle` is not
16
+ * here on purpose: an idle session can still be resumed, so a re-dispatch
17
+ * would race a live worker.
18
+ */
19
+ export const TERMINAL_WORKER_STATES = ["done", "failed", "aborted"];
20
+ export function isWorkerTerminal(state) {
21
+ return TERMINAL_WORKER_STATES.includes(state);
22
+ }
package/dist/tui.js CHANGED
@@ -14,82 +14,6 @@ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-j
14
14
  // src/protocol.ts
15
15
  import { Rpc } from "@opencode/plugin/effect";
16
16
  import { Schema } from "effect";
17
- var Definition = Rpc.define({
18
- id: "opencode.superagent",
19
- methods: {
20
- router: {
21
- input: Schema.toStandardSchemaV1(Schema.Struct({})),
22
- output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String }))
23
- },
24
- status: {
25
- input: Schema.toStandardSchemaV1(Schema.Struct({})),
26
- output: Schema.toStandardSchemaV1(Schema.Struct({
27
- router: Schema.optionalKey(Schema.String),
28
- pending: Schema.Number,
29
- workers: Schema.Array(Schema.Struct({
30
- sessionID: Schema.String,
31
- title: Schema.optionalKey(Schema.String),
32
- threads: Schema.Array(Schema.String),
33
- state: Schema.Literals(["running", "idle", "failed", "done", "aborted"])
34
- }))
35
- }))
36
- },
37
- tickets: {
38
- input: Schema.toStandardSchemaV1(Schema.Struct({
39
- threadID: Schema.optionalKey(Schema.String),
40
- status: Schema.optionalKey(Schema.String),
41
- limit: Schema.optionalKey(Schema.Number)
42
- })),
43
- output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String }))
44
- },
45
- ticket: {
46
- input: Schema.toStandardSchemaV1(Schema.Struct({ ticketID: Schema.String })),
47
- output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String }))
48
- },
49
- dispatch: {
50
- input: Schema.toStandardSchemaV1(Schema.Struct({
51
- task: Schema.String,
52
- title: Schema.optionalKey(Schema.String),
53
- threadID: Schema.optionalKey(Schema.String),
54
- agent: Schema.optionalKey(Schema.String),
55
- directory: Schema.optionalKey(Schema.String),
56
- ticketID: Schema.String
57
- })),
58
- output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
59
- errors: { "dispatch.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
60
- },
61
- trigger: {
62
- input: Schema.toStandardSchemaV1(Schema.Struct({
63
- id: Schema.optionalKey(Schema.String),
64
- source: Schema.String,
65
- threadID: Schema.String,
66
- author: Schema.optionalKey(Schema.String),
67
- text: Schema.String,
68
- title: Schema.optionalKey(Schema.String),
69
- createdAt: Schema.optionalKey(Schema.Number)
70
- })),
71
- output: Schema.toStandardSchemaV1(Schema.Struct({
72
- route: Schema.Literals(["router", "worker", "dropped"]),
73
- sessionID: Schema.optionalKey(Schema.String)
74
- }))
75
- },
76
- stop: {
77
- input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
78
- output: Schema.toStandardSchemaV1(Schema.Struct({ interrupted: Schema.Boolean }))
79
- },
80
- abort: {
81
- input: Schema.toStandardSchemaV1(Schema.Struct({
82
- sessionID: Schema.optionalKey(Schema.String),
83
- reason: Schema.optionalKey(Schema.String),
84
- all: Schema.optionalKey(Schema.Boolean),
85
- stale: Schema.optionalKey(Schema.Boolean)
86
- })),
87
- output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
88
- errors: { "abort.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
89
- }
90
- },
91
- events: {}
92
- });
93
17
 
94
18
  // src/tools.ts
95
19
  var GROUP_LABELS = {
@@ -126,6 +50,22 @@ var ROUTER_TOOL_SPECS = [
126
50
  "\u6D3E\u53D1\u6210\u529F\u5373\u7ED1\u5B9A\u5DE5\u5355\uFF1AworkerSessionID \u5199\u5165\u3001attempts+1\u3001state \u2192 in_progress\uFF1BWorker \u7ED3\u675F\u81EA\u52A8\u7ED3\u5355\u3002"
127
51
  ]
128
52
  },
53
+ {
54
+ name: "sleep",
55
+ group: "control",
56
+ description: 'Pause the router for a bounded number of seconds (default 30, range 1..600) with a real timer, never a busy loop, and return "slept Ns". In Goal mode, call it after dispatch to pause and give the Worker execution time before you check in again. Only the router may call it; tracked worker sessions neither see nor may call it.',
57
+ summary: "\u6682\u505C\u6307\u5B9A\u79D2\u6570\uFF08\u9ED8\u8BA4 30\uFF0C\u8303\u56F4 1..600\uFF09\uFF0C\u7ED9 Worker \u9884\u7559\u6267\u884C\u65F6\u95F4\u3002",
58
+ params: [{ name: "seconds", required: false, type: "number", note: "\u9ED8\u8BA4 30\uFF0C1..600 \u7684\u6574\u6570" }],
59
+ risk: "read",
60
+ permission: "status",
61
+ confirmation: "\u53EA\u8BFB\u7B49\u5F85\uFF0C\u81EA\u52A8\u6267\u884C\uFF08permission: status\uFF09\uFF1B\u975E\u6CD5\u79D2\u6570\u76F4\u63A5\u62A5\u9519\u3002",
62
+ usage: [
63
+ "Goal \u6A21\u5F0F\uFF1Adispatch \u540E\u5148 sleep\uFF0C\u518D\u56DE\u6765\u6C47\u603B\uFF0C\u4E0D\u8981\u7A7A\u8F6C\u8F6E\u8BE2 Worker\u3002",
64
+ "seconds \u7F3A\u7701 30\uFF1B\u5FC5\u987B\u662F 1..600 \u7684\u6574\u6570\uFF0C0/\u8D1F\u6570/\u8D85\u754C/\u975E\u6570\u5B57\u90FD\u62A5\u9519\u3002",
65
+ "\u771F\u5B9E\u5B9A\u65F6\u5668\uFF08Effect.sleep\uFF09\uFF1A\u7B49\u5F85\u671F\u95F4 Router \u4E0E\u5176\u4ED6\u4EFB\u52A1\u7167\u5E38\u63A8\u8FDB\u3002",
66
+ "\u4EC5 Router \u53EF\u89C1\uFF1AWorker \u4F1A\u8BDD\u7684\u5DE5\u5177\u5217\u8868\u91CC\u6CA1\u6709\u5B83\uFF0C\u76F4\u8C03\u4E5F\u4F1A\u88AB\u62D2\u7EDD\u3002"
67
+ ]
68
+ },
129
69
  {
130
70
  name: "ticket_create",
131
71
  group: "dispatch",
@@ -211,11 +151,14 @@ var ROUTER_TOOL_SPECS = [
211
151
  name: "status",
212
152
  group: "query",
213
153
  description: "Show a compact summary of the router, pending triggers and worker sessions.",
214
- summary: "\u67E5\u770B Router\u3001pending \u89E6\u53D1\u5668\u6570\u91CF\u4E0E\u5168\u90E8 Worker \u7684 thread/\u72B6\u6001/\u6700\u8FD1\u62A5\u544A\u3002",
154
+ summary: "\u67E5\u770B\u63D2\u4EF6\u7248\u672C\u3001Router\u3001pending \u89E6\u53D1\u5668\u6570\u91CF\u4E0E\u5168\u90E8 Worker \u7684 thread/\u72B6\u6001/\u6700\u8FD1\u62A5\u544A\u3002",
215
155
  params: [],
216
156
  risk: "read",
217
157
  confirmation: "\u53EA\u8BFB\uFF0C\u81EA\u52A8\u6267\u884C\uFF08permission: status\uFF09\u3002",
218
- usage: ["system \u72B6\u6001\u884C\u53EA\u7ED9\u8BA1\u6570\uFF0C\u7EC6\u8282\u7528 status / thread_detail\u3002"]
158
+ usage: [
159
+ "\u9996\u884C\u662F\u63D2\u4EF6\u7248\u672C\u4E0E\u300C\u5347\u7EA7\u540E\u9700\u91CD\u542F\u7F51\u5173\u300D\u63D0\u793A\uFF0C\u7528\u6765\u5224\u65AD\u7F51\u5173\u662F\u5426\u8FD8\u5728\u8DD1\u65E7\u4EE3\u7801\u3002",
160
+ "system \u72B6\u6001\u884C\u53EA\u7ED9\u8BA1\u6570\uFF0C\u7EC6\u8282\u7528 status / thread_detail\u3002"
161
+ ]
219
162
  },
220
163
  {
221
164
  name: "thread_detail",
@@ -422,6 +365,15 @@ function describeLimits(width = DETAIL_WIDTH) {
422
365
  }
423
366
 
424
367
  // src/ticket-view.ts
368
+ var TICKET_STATES = [
369
+ "open",
370
+ "in_progress",
371
+ "blocked",
372
+ "done",
373
+ "failed",
374
+ "cancelled",
375
+ "superseded"
376
+ ];
425
377
  var TICKET_PANEL_WIDTH = 36;
426
378
  var TICKET_TITLE_MAX = 26;
427
379
  var TICKET_TERMINAL_LIMIT = 3;
@@ -516,6 +468,89 @@ function ticketPanel(rows, width = TICKET_PANEL_WIDTH, terminalLimit = TICKET_TE
516
468
  };
517
469
  }
518
470
 
471
+ // src/protocol.ts
472
+ var Definition = Rpc.define({
473
+ id: "opencode.superagent",
474
+ methods: {
475
+ router: {
476
+ input: Schema.toStandardSchemaV1(Schema.Struct({})),
477
+ output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String }))
478
+ },
479
+ status: {
480
+ input: Schema.toStandardSchemaV1(Schema.Struct({})),
481
+ output: Schema.toStandardSchemaV1(Schema.Struct({
482
+ version: Schema.optionalKey(Schema.String),
483
+ router: Schema.optionalKey(Schema.String),
484
+ pending: Schema.Number,
485
+ workers: Schema.Array(Schema.Struct({
486
+ sessionID: Schema.String,
487
+ title: Schema.optionalKey(Schema.String),
488
+ threads: Schema.Array(Schema.String),
489
+ state: Schema.Literals(["running", "idle", "failed", "done", "aborted"])
490
+ }))
491
+ }))
492
+ },
493
+ tickets: {
494
+ input: Schema.toStandardSchemaV1(Schema.Struct({
495
+ threadID: Schema.optionalKey(Schema.String),
496
+ status: Schema.optionalKey(Schema.Literals(TICKET_STATES)),
497
+ limit: Schema.optionalKey(Schema.Number)
498
+ })),
499
+ output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
500
+ errors: {
501
+ "tickets.invalid_status": Schema.toStandardSchemaV1(Schema.Struct({ status: Schema.String }))
502
+ }
503
+ },
504
+ ticket: {
505
+ input: Schema.toStandardSchemaV1(Schema.Struct({ ticketID: Schema.String })),
506
+ output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String }))
507
+ },
508
+ dispatch: {
509
+ input: Schema.toStandardSchemaV1(Schema.Struct({
510
+ task: Schema.String,
511
+ title: Schema.optionalKey(Schema.String),
512
+ threadID: Schema.optionalKey(Schema.String),
513
+ agent: Schema.optionalKey(Schema.String),
514
+ directory: Schema.optionalKey(Schema.String),
515
+ ticketID: Schema.String
516
+ })),
517
+ output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
518
+ errors: { "dispatch.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
519
+ },
520
+ trigger: {
521
+ input: Schema.toStandardSchemaV1(Schema.Struct({
522
+ id: Schema.optionalKey(Schema.String),
523
+ source: Schema.String,
524
+ threadID: Schema.String,
525
+ author: Schema.optionalKey(Schema.String),
526
+ text: Schema.String,
527
+ title: Schema.optionalKey(Schema.String),
528
+ createdAt: Schema.optionalKey(Schema.Number)
529
+ })),
530
+ output: Schema.toStandardSchemaV1(Schema.Struct({
531
+ route: Schema.Literals(["router", "worker", "dropped"]),
532
+ sessionID: Schema.optionalKey(Schema.String)
533
+ }))
534
+ },
535
+ stop: {
536
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
537
+ output: Schema.toStandardSchemaV1(Schema.Struct({ interrupted: Schema.Boolean })),
538
+ errors: { "stop.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
539
+ },
540
+ abort: {
541
+ input: Schema.toStandardSchemaV1(Schema.Struct({
542
+ sessionID: Schema.optionalKey(Schema.String),
543
+ reason: Schema.optionalKey(Schema.String),
544
+ all: Schema.optionalKey(Schema.Boolean),
545
+ stale: Schema.optionalKey(Schema.Boolean)
546
+ })),
547
+ output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
548
+ errors: { "abort.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
549
+ }
550
+ },
551
+ events: {}
552
+ });
553
+
519
554
  // tui.tsx
520
555
  var events = [{
521
556
  thread: "T-1",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-superagent",
4
- "version": "0.1.0",
4
+ "version": "0.2.1",
5
5
  "description": "Superagent: a resident router agent that dispatches background worker agents in OpenCode",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  "url": "https://github.com/xtccc/opencode-superagent/issues"
16
16
  },
17
17
  "publishConfig": {
18
+ "registry": "https://registry.npmjs.org/",
18
19
  "access": "public"
19
20
  },
20
21
  "exports": {
@@ -42,6 +43,7 @@
42
43
  "prepare": "bun run build",
43
44
  "prepack": "bun run build",
44
45
  "test": "bun test",
46
+ "test:e2e": "bun run e2e/plugin-load.e2e.ts",
45
47
  "typecheck": "bun check --no-script",
46
48
  "typecheck:tsc": "tsc --noEmit"
47
49
  },