opencode-superagent 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +333 -2
- package/agent/superagent-router.md +7 -0
- package/agent/superagent-worker.md +10 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +115 -0
- package/dist/src/abort.d.ts +31 -0
- package/dist/src/abort.js +176 -0
- package/dist/src/agents.d.ts +51 -0
- package/dist/src/agents.js +83 -0
- package/dist/src/context.d.ts +20 -0
- package/dist/src/context.js +98 -0
- package/dist/src/dirs.d.ts +45 -0
- package/dist/src/dirs.js +129 -0
- package/dist/src/dispatch.d.ts +38 -0
- package/dist/src/dispatch.js +214 -0
- package/dist/src/format.d.ts +3 -0
- package/dist/src/format.js +14 -0
- package/dist/src/guard.d.ts +4 -0
- package/dist/src/guard.js +9 -0
- package/dist/src/prompts.d.ts +1 -0
- package/dist/src/prompts.js +36 -0
- package/dist/src/protocol.d.ts +210 -0
- package/dist/src/protocol.js +88 -0
- package/dist/src/recall.d.ts +58 -0
- package/dist/src/recall.js +309 -0
- package/dist/src/report.d.ts +72 -0
- package/dist/src/report.js +147 -0
- package/dist/src/router.d.ts +35 -0
- package/dist/src/router.js +209 -0
- package/dist/src/session-meta.d.ts +42 -0
- package/dist/src/session-meta.js +91 -0
- package/dist/src/state.d.ts +124 -0
- package/dist/src/state.js +431 -0
- package/dist/src/ticket-tools.d.ts +45 -0
- package/dist/src/ticket-tools.js +148 -0
- package/dist/src/ticket-view.d.ts +62 -0
- package/dist/src/ticket-view.js +135 -0
- package/dist/src/ticket.d.ts +246 -0
- package/dist/src/ticket.js +700 -0
- package/dist/src/tools.d.ts +85 -0
- package/dist/src/tools.js +376 -0
- package/dist/src/watchdog.d.ts +16 -0
- package/dist/src/watchdog.js +77 -0
- package/dist/src/watcher.d.ts +11 -0
- package/dist/src/watcher.js +181 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +1027 -0
- package/package.json +79 -4
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Agent } from "@opencode/plugin/effect";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { ROUTER_AGENT } from "./agents.js";
|
|
4
|
+
import { threadDirHint } from "./dirs.js";
|
|
5
|
+
import { formatTrigger } from "./format.js";
|
|
6
|
+
import { ROUTER_TITLE, superagentMetadata } from "./session-meta.js";
|
|
7
|
+
import { ROUTER_KEY, store, toSessionID } from "./state.js";
|
|
8
|
+
import { noteRouterTrigger } from "./ticket.js";
|
|
9
|
+
import { ROUTER_PERMISSIONS } from "./tools.js";
|
|
10
|
+
const MAX_FLUSH = 20;
|
|
11
|
+
/** KV prefix for crash-safe pending triggers. Owned by the router; `state.ts` stays untouched. */
|
|
12
|
+
export const PENDING_PREFIX = "pending:";
|
|
13
|
+
const pendingKey = (id) => `${PENDING_PREFIX}${id}`;
|
|
14
|
+
/** Stable queue identity: explicit id, else source/thread/creation time. */
|
|
15
|
+
function pendingID(trigger, arrivedAt) {
|
|
16
|
+
return trigger.id ?? `${trigger.source}:${trigger.threadID}:${trigger.createdAt ?? arrivedAt}`;
|
|
17
|
+
}
|
|
18
|
+
function keyOf(item) {
|
|
19
|
+
return pendingKey(pendingID(item.trigger, item.arrivedAt));
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null;
|
|
23
|
+
}
|
|
24
|
+
function optionalString(record, key) {
|
|
25
|
+
const value = record[key];
|
|
26
|
+
return typeof value === "string" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function optionalNumber(record, key) {
|
|
29
|
+
const value = record[key];
|
|
30
|
+
return typeof value === "number" ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
function decodePending(value) {
|
|
33
|
+
if (!isRecord(value))
|
|
34
|
+
return undefined;
|
|
35
|
+
const raw = value["trigger"];
|
|
36
|
+
if (!isRecord(raw))
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof raw["source"] !== "string" || typeof raw["threadID"] !== "string")
|
|
39
|
+
return undefined;
|
|
40
|
+
if (typeof raw["text"] !== "string")
|
|
41
|
+
return undefined;
|
|
42
|
+
const arrivedAt = optionalNumber(value, "arrivedAt");
|
|
43
|
+
if (arrivedAt === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
trigger: {
|
|
47
|
+
id: optionalString(raw, "id"),
|
|
48
|
+
source: raw["source"],
|
|
49
|
+
threadID: raw["threadID"],
|
|
50
|
+
author: optionalString(raw, "author"),
|
|
51
|
+
text: raw["text"],
|
|
52
|
+
title: optionalString(raw, "title"),
|
|
53
|
+
createdAt: optionalNumber(raw, "createdAt"),
|
|
54
|
+
},
|
|
55
|
+
arrivedAt,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function encodePending(item) {
|
|
59
|
+
return {
|
|
60
|
+
trigger: {
|
|
61
|
+
...(item.trigger.id === undefined ? {} : { id: item.trigger.id }),
|
|
62
|
+
source: item.trigger.source,
|
|
63
|
+
threadID: item.trigger.threadID,
|
|
64
|
+
...(item.trigger.author === undefined ? {} : { author: item.trigger.author }),
|
|
65
|
+
text: item.trigger.text,
|
|
66
|
+
...(item.trigger.title === undefined ? {} : { title: item.trigger.title }),
|
|
67
|
+
...(item.trigger.createdAt === undefined ? {} : { createdAt: item.trigger.createdAt }),
|
|
68
|
+
},
|
|
69
|
+
arrivedAt: item.arrivedAt,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Enqueue a trigger in memory and in KV. The trigger gets a stable id when
|
|
74
|
+
* the caller did not supply one, so a crash-restarted process can dedupe and
|
|
75
|
+
* rehydrate it. Same-id enqueues are ignored.
|
|
76
|
+
*/
|
|
77
|
+
export const enqueuePending = Effect.fn("superagent.enqueuePending")(function* (ctx, input) {
|
|
78
|
+
const arrivedAt = Date.now();
|
|
79
|
+
const id = pendingID(input, arrivedAt);
|
|
80
|
+
if (store.pending.some((item) => pendingID(item.trigger, item.arrivedAt) === id))
|
|
81
|
+
return;
|
|
82
|
+
const item = {
|
|
83
|
+
trigger: input.id === undefined ? { ...input, id } : { ...input },
|
|
84
|
+
arrivedAt,
|
|
85
|
+
};
|
|
86
|
+
store.pending.push(item);
|
|
87
|
+
yield* ctx.storage
|
|
88
|
+
.set(keyOf(item), encodePending(item))
|
|
89
|
+
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist pending trigger", { cause })));
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Reload unflushed triggers left in KV by a crashed process back into the
|
|
93
|
+
* memory queue. Never creates a router session: the queue waits until the
|
|
94
|
+
* next `router` RPC or trigger asks for a flush.
|
|
95
|
+
*/
|
|
96
|
+
export const hydratePending = Effect.fn("superagent.hydratePending")(function* (ctx) {
|
|
97
|
+
let after;
|
|
98
|
+
do {
|
|
99
|
+
const page = yield* ctx.storage
|
|
100
|
+
.scan({ prefix: PENDING_PREFIX, after, limit: 100 })
|
|
101
|
+
.pipe(Effect.catch(() => Effect.succeed({ entries: [], next: undefined })));
|
|
102
|
+
for (const entry of page.entries) {
|
|
103
|
+
const item = decodePending(entry.value);
|
|
104
|
+
if (item === undefined) {
|
|
105
|
+
yield* ctx.storage.remove(entry.key).pipe(Effect.catch(() => Effect.void));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const id = pendingID(item.trigger, item.arrivedAt);
|
|
109
|
+
if (store.pending.some((existing) => pendingID(existing.trigger, existing.arrivedAt) === id))
|
|
110
|
+
continue;
|
|
111
|
+
store.pending.push(item.trigger.id === undefined ? { trigger: { ...item.trigger, id }, arrivedAt: item.arrivedAt } : item);
|
|
112
|
+
}
|
|
113
|
+
after = page.next;
|
|
114
|
+
} while (after !== undefined);
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* Return the live router session, creating it when missing. A router id
|
|
118
|
+
* whose session no longer exists is forgotten before creating a new one,
|
|
119
|
+
* so a user-deleted router is recovered automatically.
|
|
120
|
+
*/
|
|
121
|
+
export const ensureRouter = Effect.fn("superagent.ensureRouter")(function* (ctx) {
|
|
122
|
+
const sessions = ctx.session;
|
|
123
|
+
const storage = ctx.storage;
|
|
124
|
+
if (store.routerID !== undefined) {
|
|
125
|
+
const cached = yield* sessions.get({ sessionID: toSessionID(store.routerID) }).pipe(Effect.option);
|
|
126
|
+
if (Option.isSome(cached))
|
|
127
|
+
return store.routerID;
|
|
128
|
+
store.routerID = undefined;
|
|
129
|
+
store.routerBusy = false;
|
|
130
|
+
yield* storage.remove(ROUTER_KEY).pipe(Effect.catch(() => Effect.void));
|
|
131
|
+
}
|
|
132
|
+
const stored = yield* storage.get(ROUTER_KEY).pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
133
|
+
if (typeof stored === "string") {
|
|
134
|
+
const info = yield* sessions.get({ sessionID: toSessionID(stored) }).pipe(Effect.option);
|
|
135
|
+
if (Option.isSome(info)) {
|
|
136
|
+
store.routerID = info.value.id;
|
|
137
|
+
return store.routerID;
|
|
138
|
+
}
|
|
139
|
+
yield* storage.remove(ROUTER_KEY).pipe(Effect.catch(() => Effect.void));
|
|
140
|
+
}
|
|
141
|
+
const agents = ctx.agent;
|
|
142
|
+
const routerAgent = yield* agents.get({ agentID: Agent.ID.make(ROUTER_AGENT) }).pipe(Effect.map((result) => result.data.id), Effect.catch((cause) => Effect.logWarning("superagent router agent missing, creating session without it", { cause }).pipe(Effect.as(undefined))));
|
|
143
|
+
const created = yield* sessions.create({
|
|
144
|
+
title: ROUTER_TITLE,
|
|
145
|
+
...(routerAgent === undefined ? {} : { agent: routerAgent }),
|
|
146
|
+
metadata: superagentMetadata("router"),
|
|
147
|
+
// Derived from the tool catalog: a tool whose permission action is not
|
|
148
|
+
// allowed here is dropped from the Router's toolset by `Tool.snapshot`,
|
|
149
|
+
// so adding a tool without its action would hide it silently.
|
|
150
|
+
permissions: ROUTER_PERMISSIONS,
|
|
151
|
+
});
|
|
152
|
+
store.routerID = created.id;
|
|
153
|
+
store.routerBusy = false;
|
|
154
|
+
yield* storage
|
|
155
|
+
.set(ROUTER_KEY, created.id)
|
|
156
|
+
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist router", { cause })));
|
|
157
|
+
return created.id;
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* Merge queued triggers into one synthetic prompt for the router. Items
|
|
161
|
+
* leave the queue (memory and KV) only after the injection succeeds; on
|
|
162
|
+
* failure they stay queued for the next attempt.
|
|
163
|
+
*/
|
|
164
|
+
export const flushPending = Effect.fn("superagent.flushPending")(function* (ctx) {
|
|
165
|
+
if (store.pending.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
const routerID = yield* ensureRouter(ctx);
|
|
168
|
+
const batch = store.pending.slice(0, MAX_FLUSH);
|
|
169
|
+
const text = [
|
|
170
|
+
`${batch.length} new event(s) for the router:`,
|
|
171
|
+
...batch.map((item) => {
|
|
172
|
+
const base = formatTrigger(item.trigger);
|
|
173
|
+
const hint = threadDirHint(item.trigger.threadID);
|
|
174
|
+
return hint === undefined ? base : `${base}\n${hint}`;
|
|
175
|
+
}),
|
|
176
|
+
].join("\n\n");
|
|
177
|
+
const delivered = yield* ctx.session
|
|
178
|
+
.synthetic({ sessionID: toSessionID(routerID), text, description: "superagent triggers", delivery: "queue" })
|
|
179
|
+
.pipe(Effect.option);
|
|
180
|
+
if (Option.isNone(delivered)) {
|
|
181
|
+
yield* Effect.logWarning("superagent failed to flush pending triggers, keeping them queued", {
|
|
182
|
+
count: batch.length,
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
store.pending.splice(0, batch.length);
|
|
187
|
+
// Remember what the Router was just told: `ticket_create` copies this
|
|
188
|
+
// trigger into the ticket's authorization (id + quote).
|
|
189
|
+
const newest = batch[batch.length - 1];
|
|
190
|
+
if (newest !== undefined)
|
|
191
|
+
noteRouterTrigger(newest.trigger);
|
|
192
|
+
for (const item of batch) {
|
|
193
|
+
yield* ctx.storage.remove(keyOf(item)).pipe(Effect.catch(() => Effect.void));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
/**
|
|
197
|
+
* Router idle hook: when the router goes idle with nothing pending in the
|
|
198
|
+
* background, flush queued triggers into the same execution. An empty queue
|
|
199
|
+
* returns without admitting input, so the hook never loops on its own.
|
|
200
|
+
*/
|
|
201
|
+
export const registerRouterIdle = Effect.fn("superagent.registerRouterIdle")(function* (ctx) {
|
|
202
|
+
yield* ctx.session.hook("experimental.idle", (event) => Effect.gen(function* () {
|
|
203
|
+
if (store.routerID === undefined || event.sessionID !== store.routerID)
|
|
204
|
+
return;
|
|
205
|
+
if (event.background.length > 0)
|
|
206
|
+
return;
|
|
207
|
+
yield* flushPending(ctx);
|
|
208
|
+
}).pipe(Effect.catch((cause) => Effect.logWarning("superagent router idle flush failed", { cause }))));
|
|
209
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/effect";
|
|
2
|
+
import { Effect, type Schema } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* Session identity for the Super-agent prototype.
|
|
5
|
+
*
|
|
6
|
+
* A session's role lives in its durable `metadata.superagent` record, not in
|
|
7
|
+
* its title: the title is user-editable and auto-generated titles are not a
|
|
8
|
+
* contract. Every session this plugin creates writes the metadata at create
|
|
9
|
+
* time, and `hydrateSessionMeta` backfills sessions created before that.
|
|
10
|
+
*/
|
|
11
|
+
export type SuperagentRole = "router" | "worker";
|
|
12
|
+
export declare const ROUTER_TITLE = "\uD83E\uDDED router-main";
|
|
13
|
+
/** The role recorded in a session's metadata, when the metadata is a Super-agent one. */
|
|
14
|
+
export declare function superagentRole(metadata: unknown): SuperagentRole | undefined;
|
|
15
|
+
export declare function isSuperagentSession(session: {
|
|
16
|
+
readonly metadata?: unknown;
|
|
17
|
+
}): boolean;
|
|
18
|
+
export declare function isRouterSession(session: {
|
|
19
|
+
readonly metadata?: unknown;
|
|
20
|
+
}): boolean;
|
|
21
|
+
export declare function isWorkerSession(session: {
|
|
22
|
+
readonly metadata?: unknown;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/** The `superagent` metadata entry written at create time. */
|
|
25
|
+
export declare function superagentMetadata(role: SuperagentRole, threads?: ReadonlyArray<string>): Record<string, Schema.Json>;
|
|
26
|
+
/**
|
|
27
|
+
* Worker session title. It is convenience only: the session stays
|
|
28
|
+
* identifiable from metadata when a user renames it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function workerTitle(input: {
|
|
31
|
+
readonly task: string;
|
|
32
|
+
readonly title?: string;
|
|
33
|
+
readonly threadID?: string;
|
|
34
|
+
}): string;
|
|
35
|
+
/**
|
|
36
|
+
* Backfill `metadata.superagent` on sessions this plugin already owns (the
|
|
37
|
+
* router id plus every recorded worker) so role detection also covers
|
|
38
|
+
* sessions created before the metadata existed. Idempotent: a session whose
|
|
39
|
+
* role already matches is left untouched, and existing metadata is merged
|
|
40
|
+
* rather than replaced.
|
|
41
|
+
*/
|
|
42
|
+
export declare const hydrateSessionMeta: (ctx: Plugin.Context) => Effect.Effect<void, never, never>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { store, toSessionID } from "./state.js";
|
|
3
|
+
const WORKER_TITLE_PREFIX = "🔧 worker-";
|
|
4
|
+
export const ROUTER_TITLE = "🧭 router-main";
|
|
5
|
+
const TITLE_SLUG_MAX = 40;
|
|
6
|
+
const THREAD_LABEL_MAX = 12;
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null;
|
|
9
|
+
}
|
|
10
|
+
/** The role recorded in a session's metadata, when the metadata is a Super-agent one. */
|
|
11
|
+
export function superagentRole(metadata) {
|
|
12
|
+
if (!isRecord(metadata))
|
|
13
|
+
return undefined;
|
|
14
|
+
const superagent = metadata["superagent"];
|
|
15
|
+
if (!isRecord(superagent))
|
|
16
|
+
return undefined;
|
|
17
|
+
const role = superagent["role"];
|
|
18
|
+
return role === "router" || role === "worker" ? role : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function isSuperagentSession(session) {
|
|
21
|
+
return superagentRole(session.metadata) !== undefined;
|
|
22
|
+
}
|
|
23
|
+
export function isRouterSession(session) {
|
|
24
|
+
return superagentRole(session.metadata) === "router";
|
|
25
|
+
}
|
|
26
|
+
export function isWorkerSession(session) {
|
|
27
|
+
return superagentRole(session.metadata) === "worker";
|
|
28
|
+
}
|
|
29
|
+
/** The `superagent` metadata entry written at create time. */
|
|
30
|
+
export function superagentMetadata(role, threads) {
|
|
31
|
+
return {
|
|
32
|
+
superagent: role === "worker" ? { role, threads: threads === undefined ? [] : [...threads] } : { role },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Short label for a thread id: the last `:`-separated segment, so
|
|
37
|
+
* `telegram:123456789` becomes `123456789` and long ids keep their tail.
|
|
38
|
+
*/
|
|
39
|
+
function threadLabel(threadID) {
|
|
40
|
+
const segment = (threadID.split(":").pop() ?? threadID).trim();
|
|
41
|
+
if (segment === "")
|
|
42
|
+
return "thread";
|
|
43
|
+
return segment.length > THREAD_LABEL_MAX ? segment.slice(-THREAD_LABEL_MAX) : segment;
|
|
44
|
+
}
|
|
45
|
+
function titleSlug(text) {
|
|
46
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
47
|
+
if (clean === "")
|
|
48
|
+
return "task";
|
|
49
|
+
return clean.length > TITLE_SLUG_MAX ? `${clean.slice(0, TITLE_SLUG_MAX - 1).trimEnd()}…` : clean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Worker session title. It is convenience only: the session stays
|
|
53
|
+
* identifiable from metadata when a user renames it.
|
|
54
|
+
*/
|
|
55
|
+
export function workerTitle(input) {
|
|
56
|
+
const subject = titleSlug(input.title ?? input.task.split("\n")[0] ?? "");
|
|
57
|
+
const label = input.threadID === undefined ? subject : `${threadLabel(input.threadID)}-${subject}`;
|
|
58
|
+
return `${WORKER_TITLE_PREFIX}${label}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Backfill `metadata.superagent` on sessions this plugin already owns (the
|
|
62
|
+
* router id plus every recorded worker) so role detection also covers
|
|
63
|
+
* sessions created before the metadata existed. Idempotent: a session whose
|
|
64
|
+
* role already matches is left untouched, and existing metadata is merged
|
|
65
|
+
* rather than replaced.
|
|
66
|
+
*/
|
|
67
|
+
export const hydrateSessionMeta = Effect.fn("superagent.hydrateSessionMeta")(function* (ctx) {
|
|
68
|
+
const targets = [];
|
|
69
|
+
if (store.routerID !== undefined)
|
|
70
|
+
targets.push({ sessionID: store.routerID, role: "router" });
|
|
71
|
+
for (const worker of store.workers.values())
|
|
72
|
+
targets.push({ sessionID: worker.sessionID, role: "worker" });
|
|
73
|
+
for (const target of targets) {
|
|
74
|
+
const current = yield* ctx.session.get({ sessionID: toSessionID(target.sessionID) }).pipe(Effect.option);
|
|
75
|
+
if (Option.isNone(current))
|
|
76
|
+
continue;
|
|
77
|
+
const metadata = current.value.metadata;
|
|
78
|
+
if (superagentRole(metadata) === target.role)
|
|
79
|
+
continue;
|
|
80
|
+
const threads = target.role === "worker" ? store.workers.get(target.sessionID)?.threads : undefined;
|
|
81
|
+
yield* ctx.session
|
|
82
|
+
.update({
|
|
83
|
+
sessionID: toSessionID(target.sessionID),
|
|
84
|
+
metadata: { ...(metadata ?? {}), ...superagentMetadata(target.role, threads) },
|
|
85
|
+
})
|
|
86
|
+
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to backfill session metadata", {
|
|
87
|
+
sessionID: target.sessionID,
|
|
88
|
+
cause,
|
|
89
|
+
})));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/effect";
|
|
2
|
+
import type { Session } from "@opencode/schema/session";
|
|
3
|
+
import { Effect, type Schema } from "effect";
|
|
4
|
+
export type WorkerState = "running" | "idle" | "failed" | "done" | "aborted";
|
|
5
|
+
export interface WorkerRecord {
|
|
6
|
+
readonly sessionID: string;
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Primary thread this worker was dispatched for. Legacy records decode to
|
|
10
|
+
* `""` and `hydrate` backfills it from `thread.workers`, so every live record
|
|
11
|
+
* carries a forward link to its thread without losing the multi-thread
|
|
12
|
+
* `threads` list.
|
|
13
|
+
*/
|
|
14
|
+
threadID: string;
|
|
15
|
+
/** Trigger that authorized the dispatch, when the record knows it. */
|
|
16
|
+
triggerID?: string;
|
|
17
|
+
readonly threads: Array<string>;
|
|
18
|
+
state: WorkerState;
|
|
19
|
+
readonly createdAt: number;
|
|
20
|
+
/** Last persisted write. Refreshed by `saveWorker`, so an aborted record keeps a traceable timestamp. */
|
|
21
|
+
updatedAt?: number;
|
|
22
|
+
lastReport?: string;
|
|
23
|
+
directory?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ThreadRecord {
|
|
26
|
+
readonly threadID: string;
|
|
27
|
+
readonly workers: Array<string>;
|
|
28
|
+
readonly updatedAt: number;
|
|
29
|
+
}
|
|
30
|
+
export interface TriggerInput {
|
|
31
|
+
readonly id?: string;
|
|
32
|
+
readonly source: string;
|
|
33
|
+
readonly threadID: string;
|
|
34
|
+
readonly author?: string;
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly title?: string;
|
|
37
|
+
readonly createdAt?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface PendingTrigger {
|
|
40
|
+
readonly trigger: TriggerInput;
|
|
41
|
+
readonly arrivedAt: number;
|
|
42
|
+
}
|
|
43
|
+
export type TriggerRoute = "router" | "worker";
|
|
44
|
+
export interface TriggerRecord {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly source: string;
|
|
47
|
+
readonly threadID: string;
|
|
48
|
+
readonly author?: string;
|
|
49
|
+
readonly text: string;
|
|
50
|
+
readonly title?: string;
|
|
51
|
+
readonly createdAt: number;
|
|
52
|
+
route?: TriggerRoute;
|
|
53
|
+
sessionID?: string;
|
|
54
|
+
/** Workers dispatched from this trigger (reverse of `WorkerRecord.triggerID`). */
|
|
55
|
+
workerIDs?: Array<string>;
|
|
56
|
+
}
|
|
57
|
+
export interface DirectTriggerInput {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly source: string;
|
|
60
|
+
readonly threadID: string;
|
|
61
|
+
readonly author?: string;
|
|
62
|
+
readonly text: string;
|
|
63
|
+
readonly createdAt: number;
|
|
64
|
+
}
|
|
65
|
+
export declare function triggerIdFor(input: TriggerInput): string;
|
|
66
|
+
/**
|
|
67
|
+
* Queue that transparently records trigger history. `server.ts` and
|
|
68
|
+
* `router.ts` keep using plain `push`/`splice`; interception here writes
|
|
69
|
+
* the memory map synchronously and persists to KV in the background, so
|
|
70
|
+
* no caller changes are needed.
|
|
71
|
+
*/
|
|
72
|
+
export declare class PendingQueue extends Array<PendingTrigger> {
|
|
73
|
+
push(...items: Array<PendingTrigger>): number;
|
|
74
|
+
splice(start: number, deleteCount?: number, ...items: Array<PendingTrigger>): Array<PendingTrigger>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record a trigger delivered straight to a worker (it never passes the
|
|
78
|
+
* pending queue). The text comes from the admitted prompt, so it carries
|
|
79
|
+
* the same truncation as the worker-visible format; queued triggers keep
|
|
80
|
+
* their full text via `rememberQueued`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function upsertDirectTrigger(input: DirectTriggerInput, sessionID: string): TriggerRecord;
|
|
83
|
+
export interface Store {
|
|
84
|
+
routerID: string | undefined;
|
|
85
|
+
routerBusy: boolean;
|
|
86
|
+
readonly workers: Map<string, WorkerRecord>;
|
|
87
|
+
readonly threads: Map<string, Array<string>>;
|
|
88
|
+
readonly pending: Array<PendingTrigger>;
|
|
89
|
+
readonly triggers: Map<string, TriggerRecord>;
|
|
90
|
+
triggerTotal: number;
|
|
91
|
+
}
|
|
92
|
+
export declare const store: Store;
|
|
93
|
+
export declare const ROUTER_KEY = "router";
|
|
94
|
+
export declare const WORKER_PREFIX = "worker:";
|
|
95
|
+
export declare const THREAD_PREFIX = "thread:";
|
|
96
|
+
export declare const TRIGGER_PREFIX = "trigger:";
|
|
97
|
+
export declare const workerKey: (sessionID: string) => string;
|
|
98
|
+
export declare const threadKey: (threadID: string) => string;
|
|
99
|
+
export declare const triggerKey: (id: string) => string;
|
|
100
|
+
/** Brands are compile-time only, so a validated session id string converts freely. */
|
|
101
|
+
export declare const toSessionID: (id: string) => Session.ID;
|
|
102
|
+
export declare function decodeWorker(value: Schema.Json | undefined): WorkerRecord | undefined;
|
|
103
|
+
export declare function decodeThread(value: Schema.Json | undefined): ThreadRecord | undefined;
|
|
104
|
+
export declare function decodeTrigger(value: Schema.Json | undefined): TriggerRecord | undefined;
|
|
105
|
+
export declare function encodeWorker(record: WorkerRecord): Schema.Json;
|
|
106
|
+
export declare function encodeThread(record: ThreadRecord): Schema.Json;
|
|
107
|
+
export declare function encodeTrigger(record: TriggerRecord): Schema.Json;
|
|
108
|
+
/**
|
|
109
|
+
* Load KV state into the in-memory maps. Storage is the unique source of
|
|
110
|
+
* truth; every loaded session is validated with `session.get` and stale
|
|
111
|
+
* entries are cleaned from both memory and KV.
|
|
112
|
+
*/
|
|
113
|
+
export declare const hydrate: (ctx: Plugin.Context) => Effect.Effect<void, never, never>;
|
|
114
|
+
/** Every worker write refreshes `updatedAt`, so an aborted record keeps a traceable timestamp. */
|
|
115
|
+
export declare const saveWorker: (ctx: Plugin.Context, record: WorkerRecord) => Effect.Effect<void, never, never>;
|
|
116
|
+
export declare const saveTrigger: (ctx: Plugin.Context, record: TriggerRecord) => Effect.Effect<void, never, never>;
|
|
117
|
+
/**
|
|
118
|
+
* Record the worker on the trigger that authorized it (`trigger.workerIDs`),
|
|
119
|
+
* completing the reverse edge of `WorkerRecord.triggerID`. Memory holds only
|
|
120
|
+
* the newest triggers, so an older trigger is read from KV once; a missing
|
|
121
|
+
* trigger is not invented. Repeating the call is a no-op.
|
|
122
|
+
*/
|
|
123
|
+
export declare const linkTriggerToWorker: (ctx: Plugin.Context, triggerID: string, sessionID: string) => Effect.Effect<boolean, never, never>;
|
|
124
|
+
export declare const saveThread: (ctx: Plugin.Context, record: ThreadRecord) => Effect.Effect<void, never, never>;
|