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.
- package/README.md +24 -10
- package/dist/server.d.ts +237 -0
- package/dist/server.js +56 -24
- 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.d.ts +6 -0
- package/dist/src/dispatch.js +100 -11
- package/dist/src/prompts.d.ts +1 -1
- package/dist/src/prompts.js +2 -1
- package/dist/src/protocol.d.ts +24 -7
- package/dist/src/protocol.js +24 -6
- 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 +4 -21
- package/dist/src/state.js +55 -4
- package/dist/src/ticket-state.d.ts +97 -0
- package/dist/src/ticket-state.js +70 -0
- package/dist/src/ticket.d.ts +20 -60
- package/dist/src/ticket.js +40 -49
- package/dist/src/tools.js +21 -2
- package/dist/src/version.d.ts +28 -0
- package/dist/src/version.js +76 -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 +19 -33
- package/dist/src/worker-state.d.ts +41 -0
- package/dist/src/worker-state.js +22 -0
- package/dist/tui.js +113 -78
- package/package.json +3 -1
|
@@ -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
|
+
}
|
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,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
|
-
|
|
79
|
+
const closeOutcome = closeOutcomeForWorkerState(state);
|
|
80
|
+
if (closeOutcome !== undefined) {
|
|
91
81
|
yield* closeTicketsForWorker(ctx, {
|
|
92
82
|
sessionID,
|
|
93
|
-
outcome:
|
|
94
|
-
summary: worker.lastReport ?? report ?? detail ?? `${
|
|
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
|
-
|
|
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
|
|
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: [
|
|
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
|
|
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
|
},
|