opencode-superagent 0.2.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 +16 -5
- package/dist/server.d.ts +87 -84
- package/dist/server.js +13 -14
- package/dist/src/dispatch.d.ts +6 -0
- package/dist/src/dispatch.js +8 -1
- package/dist/src/protocol.d.ts +12 -4
- package/dist/src/protocol.js +16 -5
- package/dist/src/state.d.ts +2 -21
- package/dist/src/state.js +8 -6
- package/dist/src/ticket-state.d.ts +97 -0
- package/dist/src/ticket-state.js +70 -0
- package/dist/src/ticket.d.ts +19 -66
- package/dist/src/ticket.js +28 -54
- package/dist/src/tools.js +5 -2
- package/dist/src/version.d.ts +28 -0
- package/dist/src/version.js +76 -0
- package/dist/src/watcher.js +5 -3
- package/dist/src/worker-state.d.ts +41 -0
- package/dist/src/worker-state.js +22 -0
- package/dist/tui.js +8 -3
- package/package.json +2 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { WorkerState } from "./worker-state.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure ticket domain model: records, the state machine, and the selectors
|
|
4
|
+
* shared by the ticket store, the dispatcher and the worker lifecycle.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here touches the KV store, Effect or the worker registry, so this
|
|
7
|
+
* module can be imported from any layer without recreating the state ↔ ticket
|
|
8
|
+
* import cycle. `ticket.ts` keeps the effectful store and re-exports these
|
|
9
|
+
* names, so its public API is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export type TicketState = "open" | "in_progress" | "blocked" | "done" | "failed" | "cancelled" | "superseded";
|
|
12
|
+
export type TicketPriority = "p0" | "p1" | "p2";
|
|
13
|
+
export type TicketCloseOutcome = "done" | "failed" | "cancelled";
|
|
14
|
+
export interface TicketRequester {
|
|
15
|
+
readonly source: string;
|
|
16
|
+
readonly threadID: string;
|
|
17
|
+
readonly author?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Which trigger authorized the ticket: the id plus a short quote of what the user said. */
|
|
20
|
+
export interface TicketAuthorization {
|
|
21
|
+
readonly triggerID: string;
|
|
22
|
+
readonly quote: string;
|
|
23
|
+
readonly at: number;
|
|
24
|
+
}
|
|
25
|
+
export interface TicketAttempt {
|
|
26
|
+
readonly at: number;
|
|
27
|
+
readonly sessionID?: string;
|
|
28
|
+
readonly note?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TicketHistoryEntry {
|
|
31
|
+
readonly at: number;
|
|
32
|
+
readonly kind: "created" | "state" | "note" | "dispatch" | "closed";
|
|
33
|
+
readonly from?: TicketState;
|
|
34
|
+
readonly to?: TicketState;
|
|
35
|
+
readonly note?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Ticket {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
state: TicketState;
|
|
40
|
+
readonly title: string;
|
|
41
|
+
readonly objective: string;
|
|
42
|
+
readonly acceptance: ReadonlyArray<string>;
|
|
43
|
+
readonly requester: TicketRequester;
|
|
44
|
+
readonly authorization: TicketAuthorization;
|
|
45
|
+
readonly threadID: string;
|
|
46
|
+
workerSessionID?: string;
|
|
47
|
+
readonly attempts: Array<TicketAttempt>;
|
|
48
|
+
readonly priority: TicketPriority;
|
|
49
|
+
directory?: string;
|
|
50
|
+
readonly dedupeKey: string;
|
|
51
|
+
readonly parentTicketID?: string;
|
|
52
|
+
readonly tags: Array<string>;
|
|
53
|
+
readonly createdAt: number;
|
|
54
|
+
updatedAt: number;
|
|
55
|
+
closedAt?: number;
|
|
56
|
+
closeReason?: string;
|
|
57
|
+
readonly history: Array<TicketHistoryEntry>;
|
|
58
|
+
}
|
|
59
|
+
export declare const PRIORITIES: ReadonlyArray<TicketPriority>;
|
|
60
|
+
export declare const TICKET_STATES: ReadonlyArray<TicketState>;
|
|
61
|
+
/** A ticket can be dispatched while it is one of these. */
|
|
62
|
+
export declare const ACTIVE_STATES: ReadonlyArray<TicketState>;
|
|
63
|
+
export declare function isTerminalState(state: TicketState): boolean;
|
|
64
|
+
export declare function allowedTransitions(state: TicketState): ReadonlyArray<TicketState>;
|
|
65
|
+
/** Same-state moves are no-ops, not transitions. */
|
|
66
|
+
export declare function canTransition(from: TicketState, to: TicketState): boolean;
|
|
67
|
+
/** Human-readable reason a move is illegal, or `undefined` when it is allowed. */
|
|
68
|
+
export declare function transitionError(from: TicketState, to: TicketState): string | undefined;
|
|
69
|
+
/** Minimal shape a worker-bound ticket needs, so the selector stays pure and generic. */
|
|
70
|
+
export interface WorkerBoundTicket {
|
|
71
|
+
readonly workerSessionID?: string;
|
|
72
|
+
readonly state: TicketState;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The `in_progress` ticket attached to a worker session, if any. Shared by the
|
|
76
|
+
* dispatch link and worker-end closing so both sides agree on "active ticket
|
|
77
|
+
* for this worker"; insertion order decides ties, as before.
|
|
78
|
+
*/
|
|
79
|
+
export declare function findActiveTicketForWorker<T extends WorkerBoundTicket>(candidates: Iterable<T>, sessionID: string): T | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Ticket outcome for a worker that just ended, or `undefined` when the worker
|
|
82
|
+
* end closes nothing. `idle`/`aborted`/`running` leave the ticket for the abort
|
|
83
|
+
* path or a deliberate re-dispatch.
|
|
84
|
+
*/
|
|
85
|
+
export declare function closeOutcomeForWorkerState(state: WorkerState): "done" | "failed" | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Reason recorded when a worker end closes its ticket: an explicit summary
|
|
88
|
+
* wins, then the caller's note, then a generated `done (worker <id>)` line.
|
|
89
|
+
* The store still compacts it to `SUMMARY_MAX` before persisting, exactly as
|
|
90
|
+
* before this helper existed.
|
|
91
|
+
*/
|
|
92
|
+
export declare function workerCloseSummary(input: {
|
|
93
|
+
readonly sessionID: string;
|
|
94
|
+
readonly outcome: "done" | "failed";
|
|
95
|
+
readonly summary?: string | undefined;
|
|
96
|
+
readonly note?: string | undefined;
|
|
97
|
+
}): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const PRIORITIES = ["p0", "p1", "p2"];
|
|
2
|
+
export const TICKET_STATES = [
|
|
3
|
+
"open",
|
|
4
|
+
"in_progress",
|
|
5
|
+
"blocked",
|
|
6
|
+
"done",
|
|
7
|
+
"failed",
|
|
8
|
+
"cancelled",
|
|
9
|
+
"superseded",
|
|
10
|
+
];
|
|
11
|
+
/** A ticket can be dispatched while it is one of these. */
|
|
12
|
+
export const ACTIVE_STATES = ["open", "in_progress"];
|
|
13
|
+
const TRANSITIONS = {
|
|
14
|
+
// A ticket can close from any active state: `done`/`failed` also cover work
|
|
15
|
+
// finished outside the worker (or a dispatch that never started).
|
|
16
|
+
open: ["in_progress", "blocked", "done", "failed", "cancelled", "superseded"],
|
|
17
|
+
in_progress: ["blocked", "done", "failed", "cancelled", "superseded"],
|
|
18
|
+
blocked: ["in_progress", "done", "failed", "cancelled", "superseded"],
|
|
19
|
+
done: [],
|
|
20
|
+
failed: [],
|
|
21
|
+
cancelled: [],
|
|
22
|
+
superseded: [],
|
|
23
|
+
};
|
|
24
|
+
export function isTerminalState(state) {
|
|
25
|
+
return TRANSITIONS[state].length === 0;
|
|
26
|
+
}
|
|
27
|
+
export function allowedTransitions(state) {
|
|
28
|
+
return TRANSITIONS[state];
|
|
29
|
+
}
|
|
30
|
+
/** Same-state moves are no-ops, not transitions. */
|
|
31
|
+
export function canTransition(from, to) {
|
|
32
|
+
return from === to || TRANSITIONS[from].includes(to);
|
|
33
|
+
}
|
|
34
|
+
/** Human-readable reason a move is illegal, or `undefined` when it is allowed. */
|
|
35
|
+
export function transitionError(from, to) {
|
|
36
|
+
if (canTransition(from, to))
|
|
37
|
+
return undefined;
|
|
38
|
+
if (isTerminalState(from))
|
|
39
|
+
return `ticket is already closed (${from}); cannot move to ${to}`;
|
|
40
|
+
return `illegal transition ${from} -> ${to}; allowed: ${TRANSITIONS[from].join(", ")}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The `in_progress` ticket attached to a worker session, if any. Shared by the
|
|
44
|
+
* dispatch link and worker-end closing so both sides agree on "active ticket
|
|
45
|
+
* for this worker"; insertion order decides ties, as before.
|
|
46
|
+
*/
|
|
47
|
+
export function findActiveTicketForWorker(candidates, sessionID) {
|
|
48
|
+
for (const ticket of candidates) {
|
|
49
|
+
if (ticket.workerSessionID === sessionID && ticket.state === "in_progress")
|
|
50
|
+
return ticket;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Ticket outcome for a worker that just ended, or `undefined` when the worker
|
|
56
|
+
* end closes nothing. `idle`/`aborted`/`running` leave the ticket for the abort
|
|
57
|
+
* path or a deliberate re-dispatch.
|
|
58
|
+
*/
|
|
59
|
+
export function closeOutcomeForWorkerState(state) {
|
|
60
|
+
return state === "done" || state === "failed" ? state : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reason recorded when a worker end closes its ticket: an explicit summary
|
|
64
|
+
* wins, then the caller's note, then a generated `done (worker <id>)` line.
|
|
65
|
+
* The store still compacts it to `SUMMARY_MAX` before persisting, exactly as
|
|
66
|
+
* before this helper existed.
|
|
67
|
+
*/
|
|
68
|
+
export function workerCloseSummary(input) {
|
|
69
|
+
return input.summary ?? input.note ?? `${input.outcome} (worker ${input.sessionID})`;
|
|
70
|
+
}
|
package/dist/src/ticket.d.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/effect";
|
|
2
2
|
import { Tool } from "@opencode/schema/tool";
|
|
3
3
|
import { Effect, type Schema } from "effect";
|
|
4
|
-
import { type
|
|
4
|
+
import { type Ticket, type TicketAuthorization, type TicketCloseOutcome, type TicketPriority, type TicketState } from "./ticket-state.js";
|
|
5
|
+
export type { Ticket, TicketAttempt, TicketAuthorization, TicketCloseOutcome, TicketHistoryEntry, TicketPriority, TicketRequester, TicketState, } from "./ticket-state.js";
|
|
6
|
+
export { ACTIVE_STATES, allowedTransitions, canTransition, isTerminalState, TICKET_STATES, transitionError, } from "./ticket-state.js";
|
|
7
|
+
export { isWorkerTerminal, TERMINAL_WORKER_STATES } from "./worker-state.js";
|
|
8
|
+
/**
|
|
9
|
+
* Structural copy of the queued trigger input. The ticket layer must not import
|
|
10
|
+
* `state.ts` (that is the cycle this module just broke), and the shape is all
|
|
11
|
+
* `noteRouterTrigger` needs.
|
|
12
|
+
*/
|
|
13
|
+
export interface TicketTriggerNoteInput {
|
|
14
|
+
readonly id?: string;
|
|
15
|
+
readonly source: string;
|
|
16
|
+
readonly threadID: string;
|
|
17
|
+
readonly author?: string;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly title?: string;
|
|
20
|
+
readonly createdAt?: number;
|
|
21
|
+
}
|
|
5
22
|
/**
|
|
6
23
|
* Ticket (work order) store for the Super-agent P0.
|
|
7
24
|
*
|
|
@@ -23,54 +40,6 @@ import { type TriggerInput, type WorkerState } from "./state.js";
|
|
|
23
40
|
*
|
|
24
41
|
* `done`, `failed`, `cancelled` and `superseded` are terminal.
|
|
25
42
|
*/
|
|
26
|
-
export type TicketState = "open" | "in_progress" | "blocked" | "done" | "failed" | "cancelled" | "superseded";
|
|
27
|
-
export type TicketPriority = "p0" | "p1" | "p2";
|
|
28
|
-
export type TicketCloseOutcome = "done" | "failed" | "cancelled";
|
|
29
|
-
export interface TicketRequester {
|
|
30
|
-
readonly source: string;
|
|
31
|
-
readonly threadID: string;
|
|
32
|
-
readonly author?: string;
|
|
33
|
-
}
|
|
34
|
-
/** Which trigger authorized the ticket: the id plus a short quote of what the user said. */
|
|
35
|
-
export interface TicketAuthorization {
|
|
36
|
-
readonly triggerID: string;
|
|
37
|
-
readonly quote: string;
|
|
38
|
-
readonly at: number;
|
|
39
|
-
}
|
|
40
|
-
export interface TicketAttempt {
|
|
41
|
-
readonly at: number;
|
|
42
|
-
readonly sessionID?: string;
|
|
43
|
-
readonly note?: string;
|
|
44
|
-
}
|
|
45
|
-
export interface TicketHistoryEntry {
|
|
46
|
-
readonly at: number;
|
|
47
|
-
readonly kind: "created" | "state" | "note" | "dispatch" | "closed";
|
|
48
|
-
readonly from?: TicketState;
|
|
49
|
-
readonly to?: TicketState;
|
|
50
|
-
readonly note?: string;
|
|
51
|
-
}
|
|
52
|
-
export interface Ticket {
|
|
53
|
-
readonly id: string;
|
|
54
|
-
state: TicketState;
|
|
55
|
-
readonly title: string;
|
|
56
|
-
readonly objective: string;
|
|
57
|
-
readonly acceptance: ReadonlyArray<string>;
|
|
58
|
-
readonly requester: TicketRequester;
|
|
59
|
-
readonly authorization: TicketAuthorization;
|
|
60
|
-
readonly threadID: string;
|
|
61
|
-
workerSessionID?: string;
|
|
62
|
-
readonly attempts: Array<TicketAttempt>;
|
|
63
|
-
readonly priority: TicketPriority;
|
|
64
|
-
directory?: string;
|
|
65
|
-
readonly dedupeKey: string;
|
|
66
|
-
readonly parentTicketID?: string;
|
|
67
|
-
readonly tags: Array<string>;
|
|
68
|
-
readonly createdAt: number;
|
|
69
|
-
updatedAt: number;
|
|
70
|
-
closedAt?: number;
|
|
71
|
-
closeReason?: string;
|
|
72
|
-
readonly history: Array<TicketHistoryEntry>;
|
|
73
|
-
}
|
|
74
43
|
export declare const TICKET_PREFIX = "ticket:";
|
|
75
44
|
export declare const TICKET_STATUS_PREFIX = "ticketidx:status:";
|
|
76
45
|
export declare const TICKET_THREAD_PREFIX = "ticketidx:thread:";
|
|
@@ -87,15 +56,6 @@ export declare const INDEX_LIMIT = 200;
|
|
|
87
56
|
export declare const MEMORY_LIMIT = 500;
|
|
88
57
|
export declare const LIST_DEFAULT = 20;
|
|
89
58
|
export declare const LIST_MAX = 100;
|
|
90
|
-
export declare const TICKET_STATES: ReadonlyArray<TicketState>;
|
|
91
|
-
/** A ticket can be dispatched while it is one of these. */
|
|
92
|
-
export declare const ACTIVE_STATES: ReadonlyArray<TicketState>;
|
|
93
|
-
export declare function isTerminalState(state: TicketState): boolean;
|
|
94
|
-
export declare function allowedTransitions(state: TicketState): ReadonlyArray<TicketState>;
|
|
95
|
-
/** Same-state moves are no-ops, not transitions. */
|
|
96
|
-
export declare function canTransition(from: TicketState, to: TicketState): boolean;
|
|
97
|
-
/** Human-readable reason a move is illegal, or `undefined` when it is allowed. */
|
|
98
|
-
export declare function transitionError(from: TicketState, to: TicketState): string | undefined;
|
|
99
59
|
/** Lowercased, whitespace-collapsed objective, so wording drift does not defeat dedupe. */
|
|
100
60
|
export declare function normalizeObjective(text: string): string;
|
|
101
61
|
export declare function dedupeKeyFor(input: {
|
|
@@ -122,7 +82,7 @@ export interface RouterTriggerNote {
|
|
|
122
82
|
readonly text: string;
|
|
123
83
|
readonly at: number;
|
|
124
84
|
}
|
|
125
|
-
export declare function noteRouterTrigger(trigger:
|
|
85
|
+
export declare function noteRouterTrigger(trigger: TicketTriggerNoteInput): void;
|
|
126
86
|
export declare function currentTrigger(): RouterTriggerNote | undefined;
|
|
127
87
|
/** Reset memory (hydrate and tests): the map and its thread index are rebuilt together. */
|
|
128
88
|
export declare function resetTickets(): void;
|
|
@@ -142,13 +102,6 @@ export declare function ticketStats(): TicketStats;
|
|
|
142
102
|
export declare function activeTicketSummary(threadID: string | undefined, limit?: number): string;
|
|
143
103
|
/** Active ticket for a worker session, used when a worker ends. */
|
|
144
104
|
export declare function activeTicketForWorker(sessionID: string): Ticket | undefined;
|
|
145
|
-
/**
|
|
146
|
-
* Worker states after which the ticket may be dispatched again. `idle` is not
|
|
147
|
-
* here on purpose: an idle session can still be resumed, so a re-dispatch
|
|
148
|
-
* would race a live worker.
|
|
149
|
-
*/
|
|
150
|
-
export declare const TERMINAL_WORKER_STATES: ReadonlyArray<WorkerState>;
|
|
151
|
-
export declare function isWorkerTerminal(state: WorkerState): boolean;
|
|
152
105
|
export declare function validateCreateInput(input: {
|
|
153
106
|
readonly threadID?: string;
|
|
154
107
|
readonly title?: string;
|
package/dist/src/ticket.js
CHANGED
|
@@ -2,8 +2,32 @@ import { Tool } from "@opencode/schema/tool";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { normalizeDirectory } from "./dirs.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ACTIVE_STATES, findActiveTicketForWorker, isTerminalState, PRIORITIES, TICKET_STATES, transitionError, workerCloseSummary, } from "./ticket-state.js";
|
|
6
|
+
import { isWorkerTerminal, workers } from "./worker-state.js";
|
|
6
7
|
import { displayWidth, wrapText } from "./tools.js";
|
|
8
|
+
export { ACTIVE_STATES, allowedTransitions, canTransition, isTerminalState, TICKET_STATES, transitionError, } from "./ticket-state.js";
|
|
9
|
+
export { isWorkerTerminal, TERMINAL_WORKER_STATES } from "./worker-state.js";
|
|
10
|
+
/**
|
|
11
|
+
* Ticket (work order) store for the Super-agent P0.
|
|
12
|
+
*
|
|
13
|
+
* A ticket is the durable statement of "what the user asked for and how we
|
|
14
|
+
* will know it is done". The Router creates one before dispatching work; the
|
|
15
|
+
* dispatch records the worker session on it and the worker's terminal state
|
|
16
|
+
* closes it. Records live in KV under `ticket:<id>`; the `ticketidx:*` keys
|
|
17
|
+
* are derived accelerators, so hydration always rebuilds them from the
|
|
18
|
+
* record prefix (a lost index never loses a ticket).
|
|
19
|
+
*
|
|
20
|
+
* State machine:
|
|
21
|
+
*
|
|
22
|
+
* open ─┬─▶ in_progress ─┬─▶ done
|
|
23
|
+
* │ ├─▶ failed
|
|
24
|
+
* │ ├─▶ blocked ──▶ in_progress
|
|
25
|
+
* ├─▶ blocked └─▶ cancelled
|
|
26
|
+
* ├─▶ cancelled
|
|
27
|
+
* └─▶ superseded
|
|
28
|
+
*
|
|
29
|
+
* `done`, `failed`, `cancelled` and `superseded` are terminal.
|
|
30
|
+
*/
|
|
7
31
|
export const TICKET_PREFIX = "ticket:";
|
|
8
32
|
export const TICKET_STATUS_PREFIX = "ticketidx:status:";
|
|
9
33
|
export const TICKET_THREAD_PREFIX = "ticketidx:thread:";
|
|
@@ -20,47 +44,6 @@ export const INDEX_LIMIT = 200;
|
|
|
20
44
|
export const MEMORY_LIMIT = 500;
|
|
21
45
|
export const LIST_DEFAULT = 20;
|
|
22
46
|
export const LIST_MAX = 100;
|
|
23
|
-
const PRIORITIES = ["p0", "p1", "p2"];
|
|
24
|
-
export const TICKET_STATES = [
|
|
25
|
-
"open",
|
|
26
|
-
"in_progress",
|
|
27
|
-
"blocked",
|
|
28
|
-
"done",
|
|
29
|
-
"failed",
|
|
30
|
-
"cancelled",
|
|
31
|
-
"superseded",
|
|
32
|
-
];
|
|
33
|
-
/** A ticket can be dispatched while it is one of these. */
|
|
34
|
-
export const ACTIVE_STATES = ["open", "in_progress"];
|
|
35
|
-
const TRANSITIONS = {
|
|
36
|
-
// A ticket can close from any active state: `done`/`failed` also cover work
|
|
37
|
-
// finished outside the worker (or a dispatch that never started).
|
|
38
|
-
open: ["in_progress", "blocked", "done", "failed", "cancelled", "superseded"],
|
|
39
|
-
in_progress: ["blocked", "done", "failed", "cancelled", "superseded"],
|
|
40
|
-
blocked: ["in_progress", "done", "failed", "cancelled", "superseded"],
|
|
41
|
-
done: [],
|
|
42
|
-
failed: [],
|
|
43
|
-
cancelled: [],
|
|
44
|
-
superseded: [],
|
|
45
|
-
};
|
|
46
|
-
export function isTerminalState(state) {
|
|
47
|
-
return TRANSITIONS[state].length === 0;
|
|
48
|
-
}
|
|
49
|
-
export function allowedTransitions(state) {
|
|
50
|
-
return TRANSITIONS[state];
|
|
51
|
-
}
|
|
52
|
-
/** Same-state moves are no-ops, not transitions. */
|
|
53
|
-
export function canTransition(from, to) {
|
|
54
|
-
return from === to || TRANSITIONS[from].includes(to);
|
|
55
|
-
}
|
|
56
|
-
/** Human-readable reason a move is illegal, or `undefined` when it is allowed. */
|
|
57
|
-
export function transitionError(from, to) {
|
|
58
|
-
if (canTransition(from, to))
|
|
59
|
-
return undefined;
|
|
60
|
-
if (isTerminalState(from))
|
|
61
|
-
return `ticket is already closed (${from}); cannot move to ${to}`;
|
|
62
|
-
return `illegal transition ${from} -> ${to}; allowed: ${TRANSITIONS[from].join(", ")}`;
|
|
63
|
-
}
|
|
64
47
|
/** Lowercased, whitespace-collapsed objective, so wording drift does not defeat dedupe. */
|
|
65
48
|
export function normalizeObjective(text) {
|
|
66
49
|
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
@@ -162,16 +145,7 @@ export function activeTicketSummary(threadID, limit = 3) {
|
|
|
162
145
|
}
|
|
163
146
|
/** Active ticket for a worker session, used when a worker ends. */
|
|
164
147
|
export function activeTicketForWorker(sessionID) {
|
|
165
|
-
return
|
|
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);
|
|
148
|
+
return findActiveTicketForWorker(tickets.values(), sessionID);
|
|
175
149
|
}
|
|
176
150
|
// ── validation ────────────────────────────────────────────────────────────────
|
|
177
151
|
export function validateCreateInput(input) {
|
|
@@ -568,7 +542,7 @@ export const requireDispatchTicket = Effect.fn("superagent.requireDispatchTicket
|
|
|
568
542
|
// a re-dispatch exactly like `running`. A worker record that is gone blocks
|
|
569
543
|
// nothing: there is no live worker left to wait for.
|
|
570
544
|
const workerID = ticket.workerSessionID;
|
|
571
|
-
const worker = workerID === undefined ? undefined :
|
|
545
|
+
const worker = workerID === undefined ? undefined : workers.get(workerID);
|
|
572
546
|
if (workerID !== undefined && worker !== undefined && !isWorkerTerminal(worker.state)) {
|
|
573
547
|
const article = worker.state === "idle" ? "an" : "a";
|
|
574
548
|
return yield* new Tool.Error({
|
|
@@ -619,7 +593,7 @@ export const closeTicketsForWorker = Effect.fn("superagent.closeTicketsForWorker
|
|
|
619
593
|
return yield* closeTicket(ctx, {
|
|
620
594
|
ticketID: ticket.id,
|
|
621
595
|
outcome: input.outcome,
|
|
622
|
-
summary:
|
|
596
|
+
summary: workerCloseSummary(input),
|
|
623
597
|
...(input.now === undefined ? {} : { now: input.now }),
|
|
624
598
|
});
|
|
625
599
|
});
|
package/dist/src/tools.js
CHANGED
|
@@ -150,11 +150,14 @@ export const ROUTER_TOOL_SPECS = [
|
|
|
150
150
|
name: "status",
|
|
151
151
|
group: "query",
|
|
152
152
|
description: "Show a compact summary of the router, pending triggers and worker sessions.",
|
|
153
|
-
summary: "
|
|
153
|
+
summary: "查看插件版本、Router、pending 触发器数量与全部 Worker 的 thread/状态/最近报告。",
|
|
154
154
|
params: [],
|
|
155
155
|
risk: "read",
|
|
156
156
|
confirmation: "只读,自动执行(permission: status)。",
|
|
157
|
-
usage: [
|
|
157
|
+
usage: [
|
|
158
|
+
"首行是插件版本与「升级后需重启网关」提示,用来判断网关是否还在跑旧代码。",
|
|
159
|
+
"system 状态行只给计数,细节用 status / thread_detail。",
|
|
160
|
+
],
|
|
158
161
|
},
|
|
159
162
|
{
|
|
160
163
|
name: "thread_detail",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin version, read from the package's own `package.json`.
|
|
3
|
+
*
|
|
4
|
+
* The version is the only thing this module exposes: it is deliberately not a
|
|
5
|
+
* diagnostics dump, so logging it can never leak config, tokens, absolute
|
|
6
|
+
* paths or session ids.
|
|
7
|
+
*/
|
|
8
|
+
/** Shown when no candidate `package.json` can be read or parsed. */
|
|
9
|
+
export declare const UNKNOWN_VERSION = "0.0.0-unknown";
|
|
10
|
+
/** Extract `version` from raw `package.json` text; `undefined` when unusable. */
|
|
11
|
+
export declare function versionFromPackageJson(raw: string): string | undefined;
|
|
12
|
+
export type ReadFile = (file: string) => string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Read the first usable candidate, falling back to {@link UNKNOWN_VERSION}.
|
|
15
|
+
*
|
|
16
|
+
* `read` is injectable so the fallback path is testable without touching the
|
|
17
|
+
* filesystem: pass `() => undefined` for a missing package, or a stub that
|
|
18
|
+
* returns unparsable JSON.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadVersion(read?: ReadFile): string;
|
|
21
|
+
/** Cached plugin version; the process never changes version at runtime. */
|
|
22
|
+
export declare function pluginVersion(): string;
|
|
23
|
+
/**
|
|
24
|
+
* One-line startup notice. Says what is loaded and that upgrading an already
|
|
25
|
+
* running gateway is not enough — the plugin has no hot reload, so a service
|
|
26
|
+
* restart is required before the new code is active.
|
|
27
|
+
*/
|
|
28
|
+
export declare function versionNotice(): string;
|
|
@@ -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/watcher.js
CHANGED
|
@@ -3,6 +3,7 @@ import { buildReport, lastAssistantText, reportNotice, reportSummary, saveReport
|
|
|
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
|
|
@@ -75,11 +76,12 @@ const handleEnded = Effect.fn("superagent.workerEnded")(function* (ctx, sessionI
|
|
|
75
76
|
// A worker that reached a terminal state closes its in-progress ticket.
|
|
76
77
|
// Interruptions (`idle`) leave it open: the abort tool closes it explicitly
|
|
77
78
|
// and a plain interrupt may still be resumed with a follow-up.
|
|
78
|
-
|
|
79
|
+
const closeOutcome = closeOutcomeForWorkerState(state);
|
|
80
|
+
if (closeOutcome !== undefined) {
|
|
79
81
|
yield* closeTicketsForWorker(ctx, {
|
|
80
82
|
sessionID,
|
|
81
|
-
outcome:
|
|
82
|
-
summary: worker.lastReport ?? report ?? detail ?? `${
|
|
83
|
+
outcome: closeOutcome,
|
|
84
|
+
summary: worker.lastReport ?? report ?? detail ?? `${closeOutcome} (worker ${sessionID})`,
|
|
83
85
|
}).pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to close ticket for ended worker", { sessionID, cause })));
|
|
84
86
|
}
|
|
85
87
|
// Keep the thread subscriptions after the worker ends: follow-ups on the
|
|
@@ -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
|
@@ -151,11 +151,14 @@ var ROUTER_TOOL_SPECS = [
|
|
|
151
151
|
name: "status",
|
|
152
152
|
group: "query",
|
|
153
153
|
description: "Show a compact summary of the router, pending triggers and worker sessions.",
|
|
154
|
-
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",
|
|
155
155
|
params: [],
|
|
156
156
|
risk: "read",
|
|
157
157
|
confirmation: "\u53EA\u8BFB\uFF0C\u81EA\u52A8\u6267\u884C\uFF08permission: status\uFF09\u3002",
|
|
158
|
-
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
|
+
]
|
|
159
162
|
},
|
|
160
163
|
{
|
|
161
164
|
name: "thread_detail",
|
|
@@ -476,6 +479,7 @@ var Definition = Rpc.define({
|
|
|
476
479
|
status: {
|
|
477
480
|
input: Schema.toStandardSchemaV1(Schema.Struct({})),
|
|
478
481
|
output: Schema.toStandardSchemaV1(Schema.Struct({
|
|
482
|
+
version: Schema.optionalKey(Schema.String),
|
|
479
483
|
router: Schema.optionalKey(Schema.String),
|
|
480
484
|
pending: Schema.Number,
|
|
481
485
|
workers: Schema.Array(Schema.Struct({
|
|
@@ -530,7 +534,8 @@ var Definition = Rpc.define({
|
|
|
530
534
|
},
|
|
531
535
|
stop: {
|
|
532
536
|
input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
|
|
533
|
-
output: Schema.toStandardSchemaV1(Schema.Struct({ interrupted: Schema.Boolean }))
|
|
537
|
+
output: Schema.toStandardSchemaV1(Schema.Struct({ interrupted: Schema.Boolean })),
|
|
538
|
+
errors: { "stop.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) }
|
|
534
539
|
},
|
|
535
540
|
abort: {
|
|
536
541
|
input: Schema.toStandardSchemaV1(Schema.Struct({
|
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.2.
|
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"prepare": "bun run build",
|
|
44
44
|
"prepack": "bun run build",
|
|
45
45
|
"test": "bun test",
|
|
46
|
+
"test:e2e": "bun run e2e/plugin-load.e2e.ts",
|
|
46
47
|
"typecheck": "bun check --no-script",
|
|
47
48
|
"typecheck:tsc": "tsc --noEmit"
|
|
48
49
|
},
|