opencode-superagent 0.1.0 → 0.2.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.
@@ -125,7 +125,10 @@ export function getTicket(ticketID) {
125
125
  /** Same-thread, same-dedupe-key ticket still open or in progress, if any. */
126
126
  export function findDuplicate(threadID, dedupeKey) {
127
127
  const ids = byThread.get(threadID);
128
- const candidates = ids === undefined ? [...tickets.values()] : [...ids].flatMap((id) => tickets.get(id) ?? []);
128
+ // No ids means this thread owns no in-memory ticket, so there is nothing to reuse.
129
+ if (ids === undefined)
130
+ return undefined;
131
+ const candidates = [...ids].flatMap((id) => tickets.get(id) ?? []);
129
132
  return candidates.find((ticket) => ticket.dedupeKey === dedupeKey && ACTIVE_STATES.includes(ticket.state));
130
133
  }
131
134
  export function isActive(ticket) {
@@ -161,6 +164,15 @@ export function activeTicketSummary(threadID, limit = 3) {
161
164
  export function activeTicketForWorker(sessionID) {
162
165
  return [...tickets.values()].find((ticket) => ticket.workerSessionID === sessionID && ticket.state === "in_progress");
163
166
  }
167
+ /**
168
+ * Worker states after which the ticket may be dispatched again. `idle` is not
169
+ * here on purpose: an idle session can still be resumed, so a re-dispatch
170
+ * would race a live worker.
171
+ */
172
+ export const TERMINAL_WORKER_STATES = ["done", "failed", "aborted"];
173
+ export function isWorkerTerminal(state) {
174
+ return TERMINAL_WORKER_STATES.includes(state);
175
+ }
164
176
  // ── validation ────────────────────────────────────────────────────────────────
165
177
  export function validateCreateInput(input) {
166
178
  if ((input.threadID ?? "").trim() === "")
@@ -525,7 +537,7 @@ export function listTickets(input = {}) {
525
537
  * Gate every dispatch must pass: a dispatch always names a ticket, and that
526
538
  * ticket must be workable right now. The rules are enforced here (not in the
527
539
  * prompt) so a Router cannot dispatch without a ticket, onto a closed ticket,
528
- * across threads, or onto a ticket that already has a running worker.
540
+ * across threads, or onto a ticket that already has a live (non-terminal) worker.
529
541
  */
530
542
  export const requireDispatchTicket = Effect.fn("superagent.requireDispatchTicket")(function* (ticketID, threadID) {
531
543
  if (ticketID === undefined || ticketID.trim() === "") {
@@ -551,11 +563,16 @@ export const requireDispatchTicket = Effect.fn("superagent.requireDispatchTicket
551
563
  });
552
564
  }
553
565
  // One ticket never runs two workers at once: the previous worker must be
554
- // terminal (aborted, failed, done) before anything is dispatched again.
566
+ // terminal (done, failed, aborted) before anything is dispatched again.
567
+ // `idle` is NOT terminal — the session may still be resumed — so it blocks
568
+ // a re-dispatch exactly like `running`. A worker record that is gone blocks
569
+ // nothing: there is no live worker left to wait for.
555
570
  const workerID = ticket.workerSessionID;
556
- if (workerID !== undefined && store.workers.get(workerID)?.state === "running") {
571
+ const worker = workerID === undefined ? undefined : store.workers.get(workerID);
572
+ if (workerID !== undefined && worker !== undefined && !isWorkerTerminal(worker.state)) {
573
+ const article = worker.state === "idle" ? "an" : "a";
557
574
  return yield* new Tool.Error({
558
- message: `ticket ${ticket.id} already has a running worker ${workerID}; wait for it or abort_session it first, then ticket_create a new ticket for the re-dispatch`,
575
+ message: `ticket ${ticket.id} already has ${article} ${worker.state} worker ${workerID}; wait for it to finish, abort_session it, or ticket_create a new ticket for the re-dispatch`,
559
576
  });
560
577
  }
561
578
  return ticket;
package/dist/src/tools.js CHANGED
@@ -49,6 +49,22 @@ export const ROUTER_TOOL_SPECS = [
49
49
  "派发成功即绑定工单:workerSessionID 写入、attempts+1、state → in_progress;Worker 结束自动结单。",
50
50
  ],
51
51
  },
52
+ {
53
+ name: "sleep",
54
+ group: "control",
55
+ 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.',
56
+ summary: "暂停指定秒数(默认 30,范围 1..600),给 Worker 预留执行时间。",
57
+ params: [{ name: "seconds", required: false, type: "number", note: "默认 30,1..600 的整数" }],
58
+ risk: "read",
59
+ permission: "status",
60
+ confirmation: "只读等待,自动执行(permission: status);非法秒数直接报错。",
61
+ usage: [
62
+ "Goal 模式:dispatch 后先 sleep,再回来汇总,不要空转轮询 Worker。",
63
+ "seconds 缺省 30;必须是 1..600 的整数,0/负数/超界/非数字都报错。",
64
+ "真实定时器(Effect.sleep):等待期间 Router 与其他任务照常推进。",
65
+ "仅 Router 可见:Worker 会话的工具列表里没有它,直调也会被拒绝。",
66
+ ],
67
+ },
52
68
  {
53
69
  name: "ticket_create",
54
70
  group: "dispatch",
@@ -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,5 +1,5 @@
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";
@@ -15,30 +15,6 @@ export const captureReport = Effect.fn("superagent.captureReport")(function* (ct
15
15
  worker.lastReport = reportSummary(record);
16
16
  return record;
17
17
  });
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
18
  function errorDetail(data) {
43
19
  if (typeof data !== "object" || data === null)
44
20
  return undefined;
@@ -66,6 +42,18 @@ function interruptDetail(data) {
66
42
  const reason = data.reason;
67
43
  return typeof reason === "string" && reason !== "" ? `interrupted (${reason})` : "interrupted";
68
44
  }
45
+ /**
46
+ * Apply a `session.idle` notification. Only a running worker may become idle:
47
+ * `done`, `failed` and `aborted` are terminal for the lifecycle, and an idle
48
+ * event must never resurrect or overwrite them.
49
+ */
50
+ export const handleIdle = Effect.fn("superagent.workerIdle")(function* (ctx, sessionID) {
51
+ const worker = store.workers.get(sessionID);
52
+ if (worker === undefined || worker.state !== "running")
53
+ return;
54
+ worker.state = "idle";
55
+ yield* saveWorker(ctx, worker);
56
+ });
69
57
  const handleEnded = Effect.fn("superagent.workerEnded")(function* (ctx, sessionID, state, detail) {
70
58
  const sessions = ctx.session;
71
59
  const worker = store.workers.get(sessionID);
@@ -167,11 +155,7 @@ export const registerWatcher = Effect.fn("superagent.registerWatcher")(function*
167
155
  return;
168
156
  }
169
157
  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);
158
+ yield* handleIdle(ctx, sessionID);
175
159
  return;
176
160
  }
177
161
  default:
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",
@@ -422,6 +362,15 @@ function describeLimits(width = DETAIL_WIDTH) {
422
362
  }
423
363
 
424
364
  // src/ticket-view.ts
365
+ var TICKET_STATES = [
366
+ "open",
367
+ "in_progress",
368
+ "blocked",
369
+ "done",
370
+ "failed",
371
+ "cancelled",
372
+ "superseded"
373
+ ];
425
374
  var TICKET_PANEL_WIDTH = 36;
426
375
  var TICKET_TITLE_MAX = 26;
427
376
  var TICKET_TERMINAL_LIMIT = 3;
@@ -516,6 +465,87 @@ function ticketPanel(rows, width = TICKET_PANEL_WIDTH, terminalLimit = TICKET_TE
516
465
  };
517
466
  }
518
467
 
468
+ // src/protocol.ts
469
+ var Definition = Rpc.define({
470
+ id: "opencode.superagent",
471
+ methods: {
472
+ router: {
473
+ input: Schema.toStandardSchemaV1(Schema.Struct({})),
474
+ output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String }))
475
+ },
476
+ status: {
477
+ input: Schema.toStandardSchemaV1(Schema.Struct({})),
478
+ output: Schema.toStandardSchemaV1(Schema.Struct({
479
+ router: Schema.optionalKey(Schema.String),
480
+ pending: Schema.Number,
481
+ workers: Schema.Array(Schema.Struct({
482
+ sessionID: Schema.String,
483
+ title: Schema.optionalKey(Schema.String),
484
+ threads: Schema.Array(Schema.String),
485
+ state: Schema.Literals(["running", "idle", "failed", "done", "aborted"])
486
+ }))
487
+ }))
488
+ },
489
+ tickets: {
490
+ input: Schema.toStandardSchemaV1(Schema.Struct({
491
+ threadID: Schema.optionalKey(Schema.String),
492
+ status: Schema.optionalKey(Schema.Literals(TICKET_STATES)),
493
+ limit: Schema.optionalKey(Schema.Number)
494
+ })),
495
+ output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
496
+ errors: {
497
+ "tickets.invalid_status": Schema.toStandardSchemaV1(Schema.Struct({ status: Schema.String }))
498
+ }
499
+ },
500
+ ticket: {
501
+ input: Schema.toStandardSchemaV1(Schema.Struct({ ticketID: Schema.String })),
502
+ output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String }))
503
+ },
504
+ dispatch: {
505
+ input: Schema.toStandardSchemaV1(Schema.Struct({
506
+ task: Schema.String,
507
+ title: Schema.optionalKey(Schema.String),
508
+ threadID: Schema.optionalKey(Schema.String),
509
+ agent: Schema.optionalKey(Schema.String),
510
+ directory: Schema.optionalKey(Schema.String),
511
+ ticketID: Schema.String
512
+ })),
513
+ output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
514
+ errors: { "dispatch.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
515
+ },
516
+ trigger: {
517
+ input: Schema.toStandardSchemaV1(Schema.Struct({
518
+ id: Schema.optionalKey(Schema.String),
519
+ source: Schema.String,
520
+ threadID: Schema.String,
521
+ author: Schema.optionalKey(Schema.String),
522
+ text: Schema.String,
523
+ title: Schema.optionalKey(Schema.String),
524
+ createdAt: Schema.optionalKey(Schema.Number)
525
+ })),
526
+ output: Schema.toStandardSchemaV1(Schema.Struct({
527
+ route: Schema.Literals(["router", "worker", "dropped"]),
528
+ sessionID: Schema.optionalKey(Schema.String)
529
+ }))
530
+ },
531
+ stop: {
532
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
533
+ output: Schema.toStandardSchemaV1(Schema.Struct({ interrupted: Schema.Boolean }))
534
+ },
535
+ abort: {
536
+ input: Schema.toStandardSchemaV1(Schema.Struct({
537
+ sessionID: Schema.optionalKey(Schema.String),
538
+ reason: Schema.optionalKey(Schema.String),
539
+ all: Schema.optionalKey(Schema.Boolean),
540
+ stale: Schema.optionalKey(Schema.Boolean)
541
+ })),
542
+ output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
543
+ errors: { "abort.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
544
+ }
545
+ },
546
+ events: {}
547
+ });
548
+
519
549
  // tui.tsx
520
550
  var events = [{
521
551
  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.0",
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": {