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.
- package/README.md +8 -5
- package/dist/server.d.ts +234 -0
- package/dist/server.js +58 -25
- package/dist/src/abort.js +8 -13
- package/dist/src/context.d.ts +10 -3
- package/dist/src/context.js +24 -8
- package/dist/src/dispatch.js +92 -10
- package/dist/src/prompts.d.ts +1 -1
- package/dist/src/prompts.js +2 -1
- package/dist/src/protocol.d.ts +12 -3
- package/dist/src/protocol.js +8 -1
- package/dist/src/report.d.ts +6 -0
- package/dist/src/report.js +29 -0
- package/dist/src/router.d.ts +2 -3
- package/dist/src/router.js +28 -3
- package/dist/src/sleep.d.ts +35 -0
- package/dist/src/sleep.js +58 -0
- package/dist/src/state.d.ts +2 -0
- package/dist/src/state.js +50 -1
- package/dist/src/ticket.d.ts +9 -2
- package/dist/src/ticket.js +22 -5
- package/dist/src/tools.js +16 -0
- package/dist/src/watchdog.d.ts +11 -4
- package/dist/src/watchdog.js +37 -20
- package/dist/src/watcher.d.ts +6 -0
- package/dist/src/watcher.js +14 -30
- package/dist/tui.js +106 -76
- package/package.json +2 -1
package/dist/src/ticket.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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",
|
package/dist/src/watchdog.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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>;
|
package/dist/src/watchdog.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import { DateTime, Effect, Option, Stream } from "effect";
|
|
2
2
|
import { ensureRouter } from "./router.js";
|
|
3
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
worker.state
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
yield*
|
|
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
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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);
|
package/dist/src/watcher.d.ts
CHANGED
|
@@ -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>;
|
package/dist/src/watcher.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|