opencode-superagent 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/server.d.ts +234 -0
- package/dist/server.js +58 -25
- package/dist/src/abort.js +8 -13
- package/dist/src/context.d.ts +10 -3
- package/dist/src/context.js +24 -8
- package/dist/src/dispatch.js +92 -10
- package/dist/src/prompts.d.ts +1 -1
- package/dist/src/prompts.js +2 -1
- package/dist/src/protocol.d.ts +12 -3
- package/dist/src/protocol.js +8 -1
- package/dist/src/report.d.ts +6 -0
- package/dist/src/report.js +29 -0
- package/dist/src/router.d.ts +2 -3
- package/dist/src/router.js +28 -3
- package/dist/src/sleep.d.ts +35 -0
- package/dist/src/sleep.js +58 -0
- package/dist/src/state.d.ts +2 -0
- package/dist/src/state.js +50 -1
- package/dist/src/ticket.d.ts +9 -2
- package/dist/src/ticket.js +22 -5
- package/dist/src/tools.js +16 -0
- package/dist/src/watchdog.d.ts +11 -4
- package/dist/src/watchdog.js +37 -20
- package/dist/src/watcher.d.ts +6 -0
- package/dist/src/watcher.js +14 -30
- package/dist/tui.js +106 -76
- package/package.json +2 -1
package/dist/src/dispatch.js
CHANGED
|
@@ -6,7 +6,7 @@ import { WORKER_AGENT } from "./agents.js";
|
|
|
6
6
|
import { normalizeDirectory, rememberDirectory, resolveDirectory } from "./dirs.js";
|
|
7
7
|
import { superagentMetadata, workerTitle } from "./session-meta.js";
|
|
8
8
|
import { encodeThread, linkTriggerToWorker, saveWorker, store, threadKey, toSessionID, workerKey } from "./state.js";
|
|
9
|
-
import { activeTicketSummary, currentTrigger, linkTicketToWorker, requireDispatchTicket, ticketStats, } from "./ticket.js";
|
|
9
|
+
import { activeTicketForWorker, activeTicketSummary, closeTicket, currentTrigger, linkTicketToWorker, requireDispatchTicket, ticketStats, } from "./ticket.js";
|
|
10
10
|
import { specFor } from "./tools.js";
|
|
11
11
|
export const DispatchInput = Schema.Struct({
|
|
12
12
|
task: Schema.String,
|
|
@@ -18,6 +18,24 @@ export const DispatchInput = Schema.Struct({
|
|
|
18
18
|
});
|
|
19
19
|
const StatusOutput = Schema.Struct({ text: Schema.String });
|
|
20
20
|
const DispatchOutput = Schema.Struct({ sessionID: Schema.String });
|
|
21
|
+
/**
|
|
22
|
+
* Tickets with a dispatch currently in flight in this process. `runDispatch`
|
|
23
|
+
* checks and inserts in one synchronous block, so two concurrent dispatches of
|
|
24
|
+
* the same ticket cannot both pass: the loser fails with an explicit error
|
|
25
|
+
* instead of creating a second worker session. The claim is released on
|
|
26
|
+
* success, failure and interruption via `Effect.ensuring`.
|
|
27
|
+
*/
|
|
28
|
+
const inFlightTickets = new Set();
|
|
29
|
+
/** Synchronous check-and-claim: only the first caller for a ticket gets `true`. */
|
|
30
|
+
function claimDispatch(ticketID) {
|
|
31
|
+
if (inFlightTickets.has(ticketID))
|
|
32
|
+
return false;
|
|
33
|
+
inFlightTickets.add(ticketID);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function releaseDispatch(ticketID) {
|
|
37
|
+
inFlightTickets.delete(ticketID);
|
|
38
|
+
}
|
|
21
39
|
/** The ticket header a worker sees first: objective and acceptance, verbatim. */
|
|
22
40
|
function ticketHeader(ticket) {
|
|
23
41
|
const acceptance = ticket.acceptance.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
@@ -45,8 +63,48 @@ function statusText() {
|
|
|
45
63
|
}
|
|
46
64
|
return lines.join("\n");
|
|
47
65
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Undo a half-created dispatch: fail the ticket bound to the worker, detach
|
|
68
|
+
* the worker from its thread affinity lists, forget the worker record and stop
|
|
69
|
+
* its session. Every step is best-effort after the first failure, so one
|
|
70
|
+
* storage problem cannot leave the whole dispatch half-rolled-back.
|
|
71
|
+
*/
|
|
72
|
+
const rollbackDispatch = Effect.fn("superagent.rollbackDispatch")(function* (ctx, sessionID, reason) {
|
|
73
|
+
// Fail the ticket while the worker → ticket edge still exists, so the
|
|
74
|
+
// Router sees why the dispatch never started instead of a stuck in_progress.
|
|
75
|
+
const ticket = activeTicketForWorker(sessionID);
|
|
76
|
+
if (ticket !== undefined) {
|
|
77
|
+
yield* closeTicket(ctx, {
|
|
78
|
+
ticketID: ticket.id,
|
|
79
|
+
outcome: "failed",
|
|
80
|
+
summary: `dispatch rollback: ${reason}`,
|
|
81
|
+
}).pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to fail ticket for rolled-back worker", {
|
|
82
|
+
sessionID,
|
|
83
|
+
ticketID: ticket.id,
|
|
84
|
+
cause,
|
|
85
|
+
})));
|
|
86
|
+
}
|
|
87
|
+
// Thread affinity is written before the prompt; drop the worker from every
|
|
88
|
+
// list it was added to so the thread falls back to the Router.
|
|
89
|
+
const worker = store.workers.get(sessionID);
|
|
90
|
+
if (worker !== undefined) {
|
|
91
|
+
for (const threadID of worker.threads) {
|
|
92
|
+
const current = store.threads.get(threadID);
|
|
93
|
+
if (current === undefined || !current.includes(sessionID))
|
|
94
|
+
continue;
|
|
95
|
+
const next = current.filter((id) => id !== sessionID);
|
|
96
|
+
if (next.length === 0) {
|
|
97
|
+
store.threads.delete(threadID);
|
|
98
|
+
yield* ctx.storage.remove(threadKey(threadID)).pipe(Effect.catch(() => Effect.void));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
store.threads.set(threadID, next);
|
|
102
|
+
yield* ctx.storage
|
|
103
|
+
.set(threadKey(threadID), encodeThread({ threadID, workers: next, updatedAt: Date.now() }))
|
|
104
|
+
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist thread", { cause })));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
50
108
|
store.workers.delete(sessionID);
|
|
51
109
|
yield* ctx.storage
|
|
52
110
|
.remove(workerKey(sessionID))
|
|
@@ -71,16 +129,32 @@ export const runDispatch = Effect.fn("superagent.dispatch")(function* (ctx, inpu
|
|
|
71
129
|
return yield* new Tool.Error({ message: "dispatch is only available to the router session" });
|
|
72
130
|
}
|
|
73
131
|
const routerID = store.routerID;
|
|
132
|
+
// Every dispatch is backed by a ticket: the gate refuses a missing ticket,
|
|
133
|
+
// an unknown/closed one, a thread mismatch and a ticket whose worker is
|
|
134
|
+
// still running, so no dispatch can bypass the work-order record.
|
|
135
|
+
const ticket = yield* requireDispatchTicket(input.ticketID, input.threadID);
|
|
136
|
+
// Claim the ticket synchronously before any further dispatch step. Check
|
|
137
|
+
// and insert happen in one uninterrupted block, so two concurrent calls can
|
|
138
|
+
// never both see the ticket as free and race into `sessions.create`; the
|
|
139
|
+
// loser gets an explicit error instead of a second worker.
|
|
140
|
+
if (!claimDispatch(ticket.id)) {
|
|
141
|
+
return yield* new Tool.Error({ message: `dispatch already in progress for ticket ${ticket.id}` });
|
|
142
|
+
}
|
|
143
|
+
return yield* completeDispatch(ctx, input, routerID, ticket).pipe(
|
|
144
|
+
// Success, failure and interruption all release the claim.
|
|
145
|
+
Effect.ensuring(Effect.sync(() => releaseDispatch(ticket.id))));
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* Body of a dispatch that already owns the ticket claim. `runDispatch` is the
|
|
149
|
+
* only caller and releases the claim on every exit path.
|
|
150
|
+
*/
|
|
151
|
+
const completeDispatch = Effect.fn("superagent.dispatchClaimed")(function* (ctx, input, routerID, ticket) {
|
|
74
152
|
const agents = ctx.agent;
|
|
75
153
|
const sessions = ctx.session;
|
|
76
154
|
const requested = input.agent ?? WORKER_AGENT;
|
|
77
155
|
if (input.directory !== undefined && normalizeDirectory(input.directory) === undefined) {
|
|
78
156
|
return yield* new Tool.Error({ message: "dispatch directory must be an absolute path, e.g. /opt/ai-gw" });
|
|
79
157
|
}
|
|
80
|
-
// Every dispatch is backed by a ticket: the gate refuses a missing ticket,
|
|
81
|
-
// an unknown/closed one, a thread mismatch and a ticket whose worker is
|
|
82
|
-
// still running, so no dispatch can bypass the work-order record.
|
|
83
|
-
const ticket = yield* requireDispatchTicket(input.ticketID, input.threadID);
|
|
84
158
|
// The ticket carries its thread; the dispatch must agree with it and
|
|
85
159
|
// inherits it when the call omits one.
|
|
86
160
|
const threadID = input.threadID ?? ticket.threadID;
|
|
@@ -162,7 +236,7 @@ export const runDispatch = Effect.fn("superagent.dispatch")(function* (ctx, inpu
|
|
|
162
236
|
...(targetDirectory === undefined ? {} : { note: targetDirectory }),
|
|
163
237
|
}).pipe(Effect.as(undefined), Effect.catchTag("Tool.Error", (error) => Effect.succeed(error.message)));
|
|
164
238
|
if (linkFailure !== undefined) {
|
|
165
|
-
yield* rollbackDispatch(ctx, created.id);
|
|
239
|
+
yield* rollbackDispatch(ctx, created.id, `could not bind ticket ${ticket.id} to the new worker (${linkFailure})`);
|
|
166
240
|
return yield* new Tool.Error({
|
|
167
241
|
message: `dispatch aborted: could not bind ticket ${ticket.id} to the new worker (${linkFailure})`,
|
|
168
242
|
});
|
|
@@ -178,13 +252,21 @@ export const runDispatch = Effect.fn("superagent.dispatch")(function* (ctx, inpu
|
|
|
178
252
|
yield* ctx.storage
|
|
179
253
|
.set(threadKey(threadID), encodeThread({ threadID, workers: [...existing], updatedAt: Date.now() }))
|
|
180
254
|
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist thread", { cause })));
|
|
181
|
-
|
|
255
|
+
// Prompt delivery is part of the dispatch: if the worker never receives its
|
|
256
|
+
// task, the ticket, thread affinity and worker record must not stay behind.
|
|
257
|
+
const promptFailure = yield* sessions
|
|
182
258
|
.prompt({
|
|
183
259
|
sessionID: created.id,
|
|
184
260
|
text: workerPrompt(threadID, input.title, input.task, ticket),
|
|
185
261
|
delivery: "queue",
|
|
186
262
|
})
|
|
187
|
-
.pipe(Effect.
|
|
263
|
+
.pipe(Effect.as(undefined), Effect.catch((cause) => Effect.succeed(String(cause))));
|
|
264
|
+
if (promptFailure !== undefined) {
|
|
265
|
+
yield* rollbackDispatch(ctx, created.id, `failed to prompt worker session: ${promptFailure}`);
|
|
266
|
+
return yield* new Tool.Error({
|
|
267
|
+
message: `dispatch aborted: failed to prompt worker session: ${promptFailure}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
188
270
|
const where = targetDirectory === undefined ? "gateway default directory" : targetDirectory;
|
|
189
271
|
return {
|
|
190
272
|
output: { sessionID: created.id },
|
package/dist/src/prompts.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ROUTER_PROMPT = "\u4F60\u662F Superagent Router\uFF0C\u53EA\u8D1F\u8D23\u4E0E\u7528\u6237\u4EA4\u4E92\u548C\u4EFB\u52A1\u5206\u6D3E\uFF0C\u4E0D\u6267\u884C\u5177\u4F53\u5DE5\u4F5C\u3002\n\n- \u6BCF\u4E2A\u72EC\u7ACB\u4EFB\u52A1\u8C03\u7528 dispatch \u5DE5\u5177\u6D3E\u7ED9 Worker\uFF0C\u53EF\u5E76\u884C\u591A\u6B21\u8C03\u7528\uFF1Bdispatch \u5F3A\u5236\u5E26 ticketID\uFF1A\u5148 ticket_create \u5EFA\u5355\uFF0C\u518D\u6D3E\u6D3B\uFF08\u89C1\u4E0B\uFF09\u3002\n- \u4F60\u53EA\u6709 dispatch\u3001ticket_create\u3001ticket_list\u3001ticket_update\u3001ticket_close\u3001status\u3001thread_detail\u3001worker_detail\u3001report_read\u3001abort_session \u5341\u4E2A\u5DE5\u5177\uFF1B\u4E0D\u8981\u5C1D\u8BD5\u8BFB\u6587\u4EF6\u3001\u8DD1\u547D\u4EE4\u3001\u6539\u4EE3\u7801\u3002\n- Worker \u5B8C\u6210\u540E\u4F1A\u6536\u5230 report\uFF1B\u6C47\u603B\u540E\u7EDF\u4E00\u56DE\u590D\u7528\u6237\uFF0C\u4E0D\u8981\u8F6C\u53D1\u539F\u59CB\u65E5\u5FD7\u3002\u4E8B\u4EF6\u91CC\u7684\u300C\u5168\u6587\u957F\u5EA6 N\uFF0C\u53EF\u7528 report_read \u8BFB\u53D6\u300D\u6307\u5411\u5B8C\u6574\u5168\u6587\uFF0C\u62A5\u544A\u88AB\u622A\u65AD\u65F6\u7528 report_read \u6309 offset \u7FFB\u9875\u8BFB\u5B8C\uFF0C\u4E0D\u8981\u51ED\u6458\u8981\u731C\u7ED3\u8BBA\u3002\n- \u7528\u6237\u5728\u540C\u4E00 thread \u8FFD\u95EE\u65F6\uFF0C\u8BE5\u4E8B\u4EF6\u4F1A\u76F4\u6295\u8D1F\u8D23\u8BE5 thread \u7684 Worker\uFF08\u8FD0\u884C\u4E2D\u76F4\u63A5\u6536\uFF0C\u5DF2\u7ED3\u675F\u4F1A\u88AB\u5524\u8D77\u7EE7\u7EED\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u53E6\u8D77\u65B0\u4EFB\u52A1\uFF1B\u65B0 thread \u624D\u9700\u8981 dispatch \u5EFA Worker\u3002\n- \u9700\u8981\u8BE6\u60C5\u65F6\u8C03 status \u67E5\u770B\u5404 Worker \u7684 thread \u5F52\u5C5E\u4E0E\u8FD1\u51B5\u3002\n- \u8BE6\u7EC6\u4FE1\u606F\u4E0D\u5728 system \u91CC\uFF0C\u9700\u8981\u65F6\u8C03 thread_detail/worker_detail/report_read\uFF08Spill\uFF09\uFF0C\u4E0D\u8981\u731C\u3002\n- \u7528\u6237\u8981\u6C42\u7EC8\u6B62\u6216\u6E05\u7406 Worker \u65F6\u8C03 abort_session\uFF1A\u5355\u4E2A\u4F1A\u8BDD\u4F20 sessionID\uFF0C\u6279\u91CF\u6E05\u7406\u5DF2\u7ED3\u675F\u7684\u4F20 stale:true\uFF0C\u5168\u90E8\u7EC8\u6B62\u4F20 all:true\uFF1B\u7EC8\u6B62\u540E\u8BE5 thread \u7684\u4E0B\u4E00\u6761\u6D88\u606F\u56DE\u843D\u5230\u4F60\u8FD9\u91CC\u91CD\u65B0 dispatch\u3002\n- \u56DE\u590D\u4FDD\u6301\u7B80\u77ED\uFF0C\u53EA\u5728\u9700\u8981\u51B3\u7B56\u6216\u9A8C\u6536\u65F6\u5411\u7528\u6237\u63D0\u95EE\u3002\n\n\u76EE\u5F55\u81EA\u7BA1\uFF08\u5355\u7F51\u5173\u591A\u76EE\u5F55\uFF0C\u7F51\u5173\u8FDB\u7A0B\u76EE\u5F55\u4E0D\u91CD\u8981\uFF09\uFF1A\n- dispatch \u652F\u6301 directory\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09\uFF1AWorker \u4F1A\u843D\u5230\u8BE5\u76EE\u5F55\u5E72\u6D3B\uFF1B\u6BCF\u6B21\u6D3E\u53D1\u5FC5\u987B\u5E26 directory\u3002\n- \u63A8\u65AD\u4F18\u5148\u7EA7\uFF1A\u6D88\u606F\u4E2D\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 /opt/ai-gw\uFF09> \u5386\u53F2 thread \u7ED1\u5B9A\uFF08\u7CFB\u7EDF\u4E8B\u4EF6\u6587\u672C\u4F1A\u6807\u6CE8\u201C\u5DF2\u77E5\u76EE\u5F55\u201D\uFF09> \u5DF2\u77E5\u522B\u540D\u6620\u5C04\uFF08\u89C1\u72B6\u6001\u884C\u7684 dirs\uFF09> \u7F51\u5173\u9ED8\u8BA4\u76EE\u5F55\u3002\n- \u522B\u540D\uFF08\u5982 ai-gw\u3001\u7F51\u5173\uFF09\u547D\u4E2D\u5DF2\u77E5\u6620\u5C04\u5219\u76F4\u63A5\u590D\u7528\u5BF9\u5E94\u76EE\u5F55\uFF1Bthread \u7ED1\u5B9A\u547D\u4E2D\u5219\u76F4\u63A5\u590D\u7528\u3002\n- \u65B0\u522B\u540D/\u65B0\u8DEF\u5F84\u9996\u6B21\u51FA\u73B0\u65F6\uFF1A\u7528\u6700\u53EF\u80FD\u7684\u76EE\u5F55\u6D3E\u53D1\uFF0C\u5E76\u5728\u56DE\u590D\u7528\u6237\u65F6\u786E\u8BA4\uFF08\u4F8B\uFF1A\u5DF2\u5728 /opt/ai-gw \u6D3E\u53D1\uFF0C\u5982\u4E0D\u5BF9\u8BF7\u544A\u8BC9\u6211\u6B63\u786E\u8DEF\u5F84\uFF0C\u6211\u4F1A\u8BB0\u4F4F\uFF09\uFF1B\u7EDD\u4E0D\u7F16\u9020\u4E0D\u5B58\u5728\u7684\u8DEF\u5F84\uFF0C\u7528\u6237\u53EA\u8BF4\u522B\u540D\u4E14\u65E0\u6620\u5C04\u65F6\u7528\u7F51\u5173\u9ED8\u8BA4\u76EE\u5F55\u6D3E\u53D1\u5E76\u8FFD\u95EE\u6B63\u786E\u8DEF\u5F84\u3002\n- \u670D\u52A1\u7AEF\u4F1A\u81EA\u52A8\u8BB0\u4F4F thread->directory \u548C\u76EE\u5F55 basename \u522B\u540D\uFF08\u5982 /opt/ai-gw \u8BB0\u4F4F ai-gw\uFF09\uFF0C\u4E0B\u6B21\u81EA\u52A8\u590D\u7528\uFF1B\u540C\u4E00 thread \u8FFD\u95EE\u9ED8\u8BA4\u56DE\u5230\u539F Worker\uFF08\u76EE\u5F55\u4E00\u81F4\uFF09\uFF0C\u6362\u76EE\u5F55\u624D\u9700\u65B0 dispatch\u3002\n- \u7528\u6237\u663E\u5F0F\u8981\u6C42\u6362\u76EE\u5F55\u65F6\u624D\u5207\uFF0C\u65E5\u5E38\u9760\u4F60\u81EA\u52A8\u63A8\u65AD\u3002\n\n\u5DE5\u5355\u4F18\u5148\uFF08\u603B\u5219\uFF0C\u5148\u4E8E\u4E0B\u9762\u7684\u5224\u5B9A\u7EC6\u5219\uFF09\uFF1A\n- \u6536\u5230\u4EFB\u52A1\u7684\u7B2C\u4E00\u52A8\u4F5C\u662F ticket_create \u767B\u8BB0\u610F\u56FE\uFF0C\u518D\u51B3\u5B9A\u5E76\u884C\u8FD8\u662F\u4E32\u884C\u3002\u5DE5\u5355\u662F\u8D26\u672C\uFF0C\u4E0D\u662F\u6D3E\u53D1\u7684\u526F\u4EA7\u54C1\uFF1A\u4EFB\u52A1\u5728\u6D3E\u53D1\u524D\u5C31\u5FC5\u987B\u5DF2\u7ECF\u5B58\u5728\u4E8E\u8D26\u672C\u91CC\u3002\n- \u4E00\u4E2A\u72EC\u7ACB\u4EFB\u52A1\u4E00\u5F20\u5355\u3002\u5927\u4EFB\u52A1\u62C6\u5B50\u4EFB\u52A1\u65F6\uFF0C\u5148\u7528 parentTicketID \u628A\u5168\u90E8\u5B50\u5355\u5EFA\u597D\uFF0C\u518D\u51B3\u5B9A\u6267\u884C\u65B9\u5F0F\u3002\n- \u7528\u6237\u4E00\u6761\u6D88\u606F\u91CC\u542B\u591A\u4E2A\u72EC\u7ACB\u4EFB\u52A1\u65F6\uFF0C\u5FC5\u987B\u62C6\u6210\u591A\u5F20\u5DE5\u5355\u5206\u522B\u767B\u8BB0\uFF08\u53EF\u7528 parentTicketID \u5173\u8054\uFF09\uFF0C\u4E0D\u5F97\u7528\u4E00\u5F20\u5355\u8986\u76D6\u591A\u4EF6\u4E8B\u3002\n- \u80FD\u5E76\u884C\u7684\u5B50\u5355\u5206\u522B dispatch \u7ED9\u4E0D\u540C Worker\uFF1B\u4E0D\u80FD\u5E76\u884C\u65F6\u540C\u6837\u5148\u5EFA\u5B8C\u5168\u90E8\u5B50\u5355\uFF0C\u518D\u7531\u4E00\u4E2A Worker \u4E32\u884C\u5B8C\u6210\u3002\u7EDD\u4E0D\u300C\u5E72\u5B8C\u518D\u8865\u5355\u300D\u3002\n- \u5B8C\u6210\u540E\u6309\u5B50\u5355\u9010\u5F20 ticket_close \u7ED3\u5355\uFF1B\u6CA1\u505A\u5B8C\u6216\u88AB\u53D6\u4EE3\u7684\u5355\u663E\u5F0F\u7F6E\u4E3A failed/cancelled/superseded\uFF0C\u4E0D\u7559\u957F\u671F in_progress\u3002\n- \u4F8B\u5916\uFF1A\u7EAF\u67E5\u8BE2\u3001\u53EA\u8BFB\u95EE\u7B54\u4E0D\u5EFA\u5355\uFF08\u4E0D\u4EA7\u751F\u4EA4\u4ED8\u7269\uFF09\uFF1B\u4E00\u65E6\u51FA\u73B0\u300C\u8981\u505A\u67D0\u4EF6\u4E8B\u300D\u7684\u610F\u56FE\u5C31\u7ACB\u523B\u5EFA\u5355\u3002\n\n\u5DE5\u5355\u5224\u5B9A\u7EC6\u5219\uFF08\u5F00\u65B0\u5355\u8FD8\u662F\u590D\u7528\uFF09\uFF1A\n1. \u5F3A\u5236\u89C4\u5219\uFF1A\u7528\u6237\u660E\u786E\u4EA4\u4EE3\u4E00\u4E2A\u65B0\u7684\u3001\u9700\u8981\u4EA4\u4ED8\u7ED3\u679C\u7684\u4EFB\u52A1\u65F6\uFF0C\u5FC5\u987B\u5148 ticket_create \u5EFA\u5355\uFF0C\u518D dispatch({ ticketID }) \u6D3E\u6D3B\u3002\u6CA1\u6709\u8C41\u514D\uFF1Adispatch \u7F3A ticketID\u3001\u5355\u4E0D\u5B58\u5728/\u5DF2\u5173\u95ED\u3001thread \u4E0D\u7B26\u3001\u6216\u8BE5\u5355\u5DF2\u6709\u8FD0\u884C\u4E2D Worker\uFF0C\u5DE5\u5177\u4F1A\u76F4\u63A5\u62A5\u9519\uFF0C\u6D3E\u6D3B\u4E0D\u4F1A\u53D1\u751F\u3002\n2. \u88AB\u62D2\u540E\u7684\u8865\u6551\uFF1A\u7F3A\u5355\u6216\u5355\u5DF2\u5173 \u2192 \u5148\u8865\u5EFA\u5355\u518D\u91CD\u6D3E\uFF1Bthread \u4E0D\u7B26 \u2192 \u7528\u8BE5\u5355\u81EA\u5DF1\u7684 threadID \u91CD\u6D3E\u6216\u4E3A\u8BE5 thread \u5EFA\u65B0\u5355\uFF1B\u5DF2\u6709\u8FD0\u884C\u4E2D Worker \u2192 \u5148\u7B49\u5B83\u7ED3\u675F\u6216 abort_session \u7EC8\u6B62\uFF08\u539F\u5355\u4F1A\u7ED3\u4E3A failed\uFF09\uFF0C\u518D ticket_create \u65B0\u5355\u91CD\u6D3E\u3002\u7EDD\u4E0D\u5141\u8BB8\u7ED5\u8FC7\u5DE5\u5355\u786C\u6D3E\u3002\n3. ticket_create \u5FC5\u987B\u5199\u6E05 objective\uFF08\u8981\u505A\u4EC0\u4E48\uFF09\u4E0E acceptance\uFF08\u600E\u4E48\u7B97\u5B8C\u6210\uFF0C1-10 \u6761\uFF09\uFF0C\u5E76\u5E26 directory\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09\uFF1Bacceptance \u60F3\u4E0D\u51FA\u6765\u5C31\u5148\u95EE\u7528\u6237\uFF0C\u7EDD\u4E0D\u5EFA\u7A7A\u5355\u6216\u5360\u4F4D\u5355\u3002\n4. \u540C\u4E00 thread \u91CD\u590D\u4EA4\u4EE3\u540C\u4E00\u4E2A\u4EFB\u52A1\u65F6\u76F4\u63A5\u5EFA\u5355\uFF0C\u5DE5\u5177\u4F1A\u6309 dedupeKey \u590D\u7528\u5DF2\u6709 open/in_progress \u5355\uFF1A\u8FD4\u56DE reused=true \u5C31\u6CBF\u7528\u8BE5 ticketID\uFF0C\u4E0D\u8981\u53E6\u5F00\u65B0\u5355\u3002\n5. \u540C\u4E00\u4E2A\u4EFB\u52A1\u62C6\u6210\u591A\u4E2A Worker \u65F6\uFF0C\u6BCF\u5355\u4E00\u4E2A ticketID\uFF1B\u7236\u4EFB\u52A1\u7528 parentTicketID \u5173\u8054\u3002\n6. \u63A8\u8FDB\u72B6\u6001\u7528 ticket_update\uFF08open \u2192 in_progress \u2192 blocked/done/failed/cancelled/superseded\uFF09\uFF0C\u4E0D\u8981\u8DF3\u6B65\uFF1BWorker \u7ED3\u675F\u4F1A\u81EA\u52A8\u7ED3\u5355\uFF0C\u9700\u8981\u4E3B\u52A8\u653E\u5F03\u6216\u6539\u5224\u65F6\u624D\u7528 ticket_close(done|failed|cancelled, summary)\u3002\n7. \u540C\u4E00 thread \u7684\u8FFD\u95EE\u82E5\u5C5E\u4E8E\u5DF2\u5F00\u5355\u4EFB\u52A1\uFF0C\u76F4\u63A5\u5728\u8BE5 thread \u7EE7\u7EED\uFF0C\u4E0D\u8981\u53E6\u5F00\u65B0\u5355\uFF1B\u53EA\u6709\u786E\u5B9E\u662F\u65B0\u4EFB\u52A1\u624D\u5EFA\u65B0\u5355\u3002\n8. \u72B6\u6001\u884C\u7ED9\u51FA\u5DE5\u5355\u8BA1\u6570\u4E0E\u5F53\u524D thread \u8FDB\u884C\u4E2D\u7684 ticketID+\u6807\u9898\uFF1B\u8981\u5B8C\u6574\u5217\u8868\u6216\u9A8C\u6536\u6807\u51C6\u65F6\u7528 ticket_list \u6216 /ticket <id>\uFF0C\u4E0D\u8981\u731C\u3002";
|
|
1
|
+
export declare const ROUTER_PROMPT = "\u4F60\u662F Superagent Router\uFF0C\u53EA\u8D1F\u8D23\u4E0E\u7528\u6237\u4EA4\u4E92\u548C\u4EFB\u52A1\u5206\u6D3E\uFF0C\u4E0D\u6267\u884C\u5177\u4F53\u5DE5\u4F5C\u3002\n\n- \u6BCF\u4E2A\u72EC\u7ACB\u4EFB\u52A1\u8C03\u7528 dispatch \u5DE5\u5177\u6D3E\u7ED9 Worker\uFF0C\u53EF\u5E76\u884C\u591A\u6B21\u8C03\u7528\uFF1Bdispatch \u5F3A\u5236\u5E26 ticketID\uFF1A\u5148 ticket_create \u5EFA\u5355\uFF0C\u518D\u6D3E\u6D3B\uFF08\u89C1\u4E0B\uFF09\u3002\n- \u4F60\u53EA\u6709 dispatch\u3001ticket_create\u3001ticket_list\u3001ticket_update\u3001ticket_close\u3001status\u3001thread_detail\u3001worker_detail\u3001report_read\u3001abort_session \u548C sleep \u5341\u4E00\u4E2A\u5DE5\u5177\uFF1B\u4E0D\u8981\u5C1D\u8BD5\u8BFB\u6587\u4EF6\u3001\u8DD1\u547D\u4EE4\u3001\u6539\u4EE3\u7801\u3002\n- Goal \u6A21\u5F0F\u4E0B dispatch \u540E\u53EF\u8C03\u7528 sleep\uFF08\u9ED8\u8BA4 30 \u79D2\uFF0C\u8303\u56F4 1..600\uFF09\u6682\u505C\uFF0C\u7ED9 Worker \u7559\u51FA\u6267\u884C\u65F6\u95F4\uFF1B\u4E0D\u8981\u7A7A\u8F6C\u8F6E\u8BE2 Worker\u3002\n- Worker \u5B8C\u6210\u540E\u4F1A\u6536\u5230 report\uFF1B\u6C47\u603B\u540E\u7EDF\u4E00\u56DE\u590D\u7528\u6237\uFF0C\u4E0D\u8981\u8F6C\u53D1\u539F\u59CB\u65E5\u5FD7\u3002\u4E8B\u4EF6\u91CC\u7684\u300C\u5168\u6587\u957F\u5EA6 N\uFF0C\u53EF\u7528 report_read \u8BFB\u53D6\u300D\u6307\u5411\u5B8C\u6574\u5168\u6587\uFF0C\u62A5\u544A\u88AB\u622A\u65AD\u65F6\u7528 report_read \u6309 offset \u7FFB\u9875\u8BFB\u5B8C\uFF0C\u4E0D\u8981\u51ED\u6458\u8981\u731C\u7ED3\u8BBA\u3002\n- \u7528\u6237\u5728\u540C\u4E00 thread \u8FFD\u95EE\u65F6\uFF0C\u8BE5\u4E8B\u4EF6\u4F1A\u76F4\u6295\u8D1F\u8D23\u8BE5 thread \u7684 Worker\uFF08\u8FD0\u884C\u4E2D\u76F4\u63A5\u6536\uFF0C\u5DF2\u7ED3\u675F\u4F1A\u88AB\u5524\u8D77\u7EE7\u7EED\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u53E6\u8D77\u65B0\u4EFB\u52A1\uFF1B\u65B0 thread \u624D\u9700\u8981 dispatch \u5EFA Worker\u3002\n- \u9700\u8981\u8BE6\u60C5\u65F6\u8C03 status \u67E5\u770B\u5404 Worker \u7684 thread \u5F52\u5C5E\u4E0E\u8FD1\u51B5\u3002\n- \u8BE6\u7EC6\u4FE1\u606F\u4E0D\u5728 system \u91CC\uFF0C\u9700\u8981\u65F6\u8C03 thread_detail/worker_detail/report_read\uFF08Spill\uFF09\uFF0C\u4E0D\u8981\u731C\u3002\n- \u7528\u6237\u8981\u6C42\u7EC8\u6B62\u6216\u6E05\u7406 Worker \u65F6\u8C03 abort_session\uFF1A\u5355\u4E2A\u4F1A\u8BDD\u4F20 sessionID\uFF0C\u6279\u91CF\u6E05\u7406\u5DF2\u7ED3\u675F\u7684\u4F20 stale:true\uFF0C\u5168\u90E8\u7EC8\u6B62\u4F20 all:true\uFF1B\u7EC8\u6B62\u540E\u8BE5 thread \u7684\u4E0B\u4E00\u6761\u6D88\u606F\u56DE\u843D\u5230\u4F60\u8FD9\u91CC\u91CD\u65B0 dispatch\u3002\n- \u56DE\u590D\u4FDD\u6301\u7B80\u77ED\uFF0C\u53EA\u5728\u9700\u8981\u51B3\u7B56\u6216\u9A8C\u6536\u65F6\u5411\u7528\u6237\u63D0\u95EE\u3002\n\n\u76EE\u5F55\u81EA\u7BA1\uFF08\u5355\u7F51\u5173\u591A\u76EE\u5F55\uFF0C\u7F51\u5173\u8FDB\u7A0B\u76EE\u5F55\u4E0D\u91CD\u8981\uFF09\uFF1A\n- dispatch \u652F\u6301 directory\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09\uFF1AWorker \u4F1A\u843D\u5230\u8BE5\u76EE\u5F55\u5E72\u6D3B\uFF1B\u6BCF\u6B21\u6D3E\u53D1\u5FC5\u987B\u5E26 directory\u3002\n- \u63A8\u65AD\u4F18\u5148\u7EA7\uFF1A\u6D88\u606F\u4E2D\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 /opt/ai-gw\uFF09> \u5386\u53F2 thread \u7ED1\u5B9A\uFF08\u7CFB\u7EDF\u4E8B\u4EF6\u6587\u672C\u4F1A\u6807\u6CE8\u201C\u5DF2\u77E5\u76EE\u5F55\u201D\uFF09> \u5DF2\u77E5\u522B\u540D\u6620\u5C04\uFF08\u89C1\u72B6\u6001\u884C\u7684 dirs\uFF09> \u7F51\u5173\u9ED8\u8BA4\u76EE\u5F55\u3002\n- \u522B\u540D\uFF08\u5982 ai-gw\u3001\u7F51\u5173\uFF09\u547D\u4E2D\u5DF2\u77E5\u6620\u5C04\u5219\u76F4\u63A5\u590D\u7528\u5BF9\u5E94\u76EE\u5F55\uFF1Bthread \u7ED1\u5B9A\u547D\u4E2D\u5219\u76F4\u63A5\u590D\u7528\u3002\n- \u65B0\u522B\u540D/\u65B0\u8DEF\u5F84\u9996\u6B21\u51FA\u73B0\u65F6\uFF1A\u7528\u6700\u53EF\u80FD\u7684\u76EE\u5F55\u6D3E\u53D1\uFF0C\u5E76\u5728\u56DE\u590D\u7528\u6237\u65F6\u786E\u8BA4\uFF08\u4F8B\uFF1A\u5DF2\u5728 /opt/ai-gw \u6D3E\u53D1\uFF0C\u5982\u4E0D\u5BF9\u8BF7\u544A\u8BC9\u6211\u6B63\u786E\u8DEF\u5F84\uFF0C\u6211\u4F1A\u8BB0\u4F4F\uFF09\uFF1B\u7EDD\u4E0D\u7F16\u9020\u4E0D\u5B58\u5728\u7684\u8DEF\u5F84\uFF0C\u7528\u6237\u53EA\u8BF4\u522B\u540D\u4E14\u65E0\u6620\u5C04\u65F6\u7528\u7F51\u5173\u9ED8\u8BA4\u76EE\u5F55\u6D3E\u53D1\u5E76\u8FFD\u95EE\u6B63\u786E\u8DEF\u5F84\u3002\n- \u670D\u52A1\u7AEF\u4F1A\u81EA\u52A8\u8BB0\u4F4F thread->directory \u548C\u76EE\u5F55 basename \u522B\u540D\uFF08\u5982 /opt/ai-gw \u8BB0\u4F4F ai-gw\uFF09\uFF0C\u4E0B\u6B21\u81EA\u52A8\u590D\u7528\uFF1B\u540C\u4E00 thread \u8FFD\u95EE\u9ED8\u8BA4\u56DE\u5230\u539F Worker\uFF08\u76EE\u5F55\u4E00\u81F4\uFF09\uFF0C\u6362\u76EE\u5F55\u624D\u9700\u65B0 dispatch\u3002\n- \u7528\u6237\u663E\u5F0F\u8981\u6C42\u6362\u76EE\u5F55\u65F6\u624D\u5207\uFF0C\u65E5\u5E38\u9760\u4F60\u81EA\u52A8\u63A8\u65AD\u3002\n\n\u5DE5\u5355\u4F18\u5148\uFF08\u603B\u5219\uFF0C\u5148\u4E8E\u4E0B\u9762\u7684\u5224\u5B9A\u7EC6\u5219\uFF09\uFF1A\n- \u6536\u5230\u4EFB\u52A1\u7684\u7B2C\u4E00\u52A8\u4F5C\u662F ticket_create \u767B\u8BB0\u610F\u56FE\uFF0C\u518D\u51B3\u5B9A\u5E76\u884C\u8FD8\u662F\u4E32\u884C\u3002\u5DE5\u5355\u662F\u8D26\u672C\uFF0C\u4E0D\u662F\u6D3E\u53D1\u7684\u526F\u4EA7\u54C1\uFF1A\u4EFB\u52A1\u5728\u6D3E\u53D1\u524D\u5C31\u5FC5\u987B\u5DF2\u7ECF\u5B58\u5728\u4E8E\u8D26\u672C\u91CC\u3002\n- \u4E00\u4E2A\u72EC\u7ACB\u4EFB\u52A1\u4E00\u5F20\u5355\u3002\u5927\u4EFB\u52A1\u62C6\u5B50\u4EFB\u52A1\u65F6\uFF0C\u5148\u7528 parentTicketID \u628A\u5168\u90E8\u5B50\u5355\u5EFA\u597D\uFF0C\u518D\u51B3\u5B9A\u6267\u884C\u65B9\u5F0F\u3002\n- \u7528\u6237\u4E00\u6761\u6D88\u606F\u91CC\u542B\u591A\u4E2A\u72EC\u7ACB\u4EFB\u52A1\u65F6\uFF0C\u5FC5\u987B\u62C6\u6210\u591A\u5F20\u5DE5\u5355\u5206\u522B\u767B\u8BB0\uFF08\u53EF\u7528 parentTicketID \u5173\u8054\uFF09\uFF0C\u4E0D\u5F97\u7528\u4E00\u5F20\u5355\u8986\u76D6\u591A\u4EF6\u4E8B\u3002\n- \u80FD\u5E76\u884C\u7684\u5B50\u5355\u5206\u522B dispatch \u7ED9\u4E0D\u540C Worker\uFF1B\u4E0D\u80FD\u5E76\u884C\u65F6\u540C\u6837\u5148\u5EFA\u5B8C\u5168\u90E8\u5B50\u5355\uFF0C\u518D\u7531\u4E00\u4E2A Worker \u4E32\u884C\u5B8C\u6210\u3002\u7EDD\u4E0D\u300C\u5E72\u5B8C\u518D\u8865\u5355\u300D\u3002\n- \u5B8C\u6210\u540E\u6309\u5B50\u5355\u9010\u5F20 ticket_close \u7ED3\u5355\uFF1B\u6CA1\u505A\u5B8C\u6216\u88AB\u53D6\u4EE3\u7684\u5355\u663E\u5F0F\u7F6E\u4E3A failed/cancelled/superseded\uFF0C\u4E0D\u7559\u957F\u671F in_progress\u3002\n- \u4F8B\u5916\uFF1A\u7EAF\u67E5\u8BE2\u3001\u53EA\u8BFB\u95EE\u7B54\u4E0D\u5EFA\u5355\uFF08\u4E0D\u4EA7\u751F\u4EA4\u4ED8\u7269\uFF09\uFF1B\u4E00\u65E6\u51FA\u73B0\u300C\u8981\u505A\u67D0\u4EF6\u4E8B\u300D\u7684\u610F\u56FE\u5C31\u7ACB\u523B\u5EFA\u5355\u3002\n\n\u5DE5\u5355\u5224\u5B9A\u7EC6\u5219\uFF08\u5F00\u65B0\u5355\u8FD8\u662F\u590D\u7528\uFF09\uFF1A\n1. \u5F3A\u5236\u89C4\u5219\uFF1A\u7528\u6237\u660E\u786E\u4EA4\u4EE3\u4E00\u4E2A\u65B0\u7684\u3001\u9700\u8981\u4EA4\u4ED8\u7ED3\u679C\u7684\u4EFB\u52A1\u65F6\uFF0C\u5FC5\u987B\u5148 ticket_create \u5EFA\u5355\uFF0C\u518D dispatch({ ticketID }) \u6D3E\u6D3B\u3002\u6CA1\u6709\u8C41\u514D\uFF1Adispatch \u7F3A ticketID\u3001\u5355\u4E0D\u5B58\u5728/\u5DF2\u5173\u95ED\u3001thread \u4E0D\u7B26\u3001\u6216\u8BE5\u5355\u5DF2\u6709\u8FD0\u884C\u4E2D Worker\uFF0C\u5DE5\u5177\u4F1A\u76F4\u63A5\u62A5\u9519\uFF0C\u6D3E\u6D3B\u4E0D\u4F1A\u53D1\u751F\u3002\n2. \u88AB\u62D2\u540E\u7684\u8865\u6551\uFF1A\u7F3A\u5355\u6216\u5355\u5DF2\u5173 \u2192 \u5148\u8865\u5EFA\u5355\u518D\u91CD\u6D3E\uFF1Bthread \u4E0D\u7B26 \u2192 \u7528\u8BE5\u5355\u81EA\u5DF1\u7684 threadID \u91CD\u6D3E\u6216\u4E3A\u8BE5 thread \u5EFA\u65B0\u5355\uFF1B\u5DF2\u6709\u8FD0\u884C\u4E2D Worker \u2192 \u5148\u7B49\u5B83\u7ED3\u675F\u6216 abort_session \u7EC8\u6B62\uFF08\u539F\u5355\u4F1A\u7ED3\u4E3A failed\uFF09\uFF0C\u518D ticket_create \u65B0\u5355\u91CD\u6D3E\u3002\u7EDD\u4E0D\u5141\u8BB8\u7ED5\u8FC7\u5DE5\u5355\u786C\u6D3E\u3002\n3. ticket_create \u5FC5\u987B\u5199\u6E05 objective\uFF08\u8981\u505A\u4EC0\u4E48\uFF09\u4E0E acceptance\uFF08\u600E\u4E48\u7B97\u5B8C\u6210\uFF0C1-10 \u6761\uFF09\uFF0C\u5E76\u5E26 directory\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09\uFF1Bacceptance \u60F3\u4E0D\u51FA\u6765\u5C31\u5148\u95EE\u7528\u6237\uFF0C\u7EDD\u4E0D\u5EFA\u7A7A\u5355\u6216\u5360\u4F4D\u5355\u3002\n4. \u540C\u4E00 thread \u91CD\u590D\u4EA4\u4EE3\u540C\u4E00\u4E2A\u4EFB\u52A1\u65F6\u76F4\u63A5\u5EFA\u5355\uFF0C\u5DE5\u5177\u4F1A\u6309 dedupeKey \u590D\u7528\u5DF2\u6709 open/in_progress \u5355\uFF1A\u8FD4\u56DE reused=true \u5C31\u6CBF\u7528\u8BE5 ticketID\uFF0C\u4E0D\u8981\u53E6\u5F00\u65B0\u5355\u3002\n5. \u540C\u4E00\u4E2A\u4EFB\u52A1\u62C6\u6210\u591A\u4E2A Worker \u65F6\uFF0C\u6BCF\u5355\u4E00\u4E2A ticketID\uFF1B\u7236\u4EFB\u52A1\u7528 parentTicketID \u5173\u8054\u3002\n6. \u63A8\u8FDB\u72B6\u6001\u7528 ticket_update\uFF08open \u2192 in_progress \u2192 blocked/done/failed/cancelled/superseded\uFF09\uFF0C\u4E0D\u8981\u8DF3\u6B65\uFF1BWorker \u7ED3\u675F\u4F1A\u81EA\u52A8\u7ED3\u5355\uFF0C\u9700\u8981\u4E3B\u52A8\u653E\u5F03\u6216\u6539\u5224\u65F6\u624D\u7528 ticket_close(done|failed|cancelled, summary)\u3002\n7. \u540C\u4E00 thread \u7684\u8FFD\u95EE\u82E5\u5C5E\u4E8E\u5DF2\u5F00\u5355\u4EFB\u52A1\uFF0C\u76F4\u63A5\u5728\u8BE5 thread \u7EE7\u7EED\uFF0C\u4E0D\u8981\u53E6\u5F00\u65B0\u5355\uFF1B\u53EA\u6709\u786E\u5B9E\u662F\u65B0\u4EFB\u52A1\u624D\u5EFA\u65B0\u5355\u3002\n8. \u72B6\u6001\u884C\u7ED9\u51FA\u5DE5\u5355\u8BA1\u6570\u4E0E\u5F53\u524D thread \u8FDB\u884C\u4E2D\u7684 ticketID+\u6807\u9898\uFF1B\u8981\u5B8C\u6574\u5217\u8868\u6216\u9A8C\u6536\u6807\u51C6\u65F6\u7528 ticket_list \u6216 /ticket <id>\uFF0C\u4E0D\u8981\u731C\u3002";
|
package/dist/src/prompts.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const ROUTER_PROMPT = `你是 Superagent Router,只负责与用户交互和任务分派,不执行具体工作。
|
|
2
2
|
|
|
3
3
|
- 每个独立任务调用 dispatch 工具派给 Worker,可并行多次调用;dispatch 强制带 ticketID:先 ticket_create 建单,再派活(见下)。
|
|
4
|
-
- 你只有 dispatch、ticket_create、ticket_list、ticket_update、ticket_close、status、thread_detail、worker_detail、report_read、abort_session
|
|
4
|
+
- 你只有 dispatch、ticket_create、ticket_list、ticket_update、ticket_close、status、thread_detail、worker_detail、report_read、abort_session 和 sleep 十一个工具;不要尝试读文件、跑命令、改代码。
|
|
5
|
+
- Goal 模式下 dispatch 后可调用 sleep(默认 30 秒,范围 1..600)暂停,给 Worker 留出执行时间;不要空转轮询 Worker。
|
|
5
6
|
- Worker 完成后会收到 report;汇总后统一回复用户,不要转发原始日志。事件里的「全文长度 N,可用 report_read 读取」指向完整全文,报告被截断时用 report_read 按 offset 翻页读完,不要凭摘要猜结论。
|
|
6
7
|
- 用户在同一 thread 追问时,该事件会直投负责该 thread 的 Worker(运行中直接收,已结束会被唤起继续),除非用户明确另起新任务;新 thread 才需要 dispatch 建 Worker。
|
|
7
8
|
- 需要详情时调 status 查看各 Worker 的 thread 归属与近况。
|
package/dist/src/protocol.d.ts
CHANGED
|
@@ -55,15 +55,15 @@ export declare const Definition: {
|
|
|
55
55
|
tickets: {
|
|
56
56
|
input: import("effect/StandardSchema").StandardSchemaV1<{
|
|
57
57
|
readonly threadID?: string | undefined;
|
|
58
|
-
readonly status?:
|
|
58
|
+
readonly status?: "blocked" | "cancelled" | "done" | "failed" | "in_progress" | "open" | "superseded" | undefined;
|
|
59
59
|
readonly limit?: number | undefined;
|
|
60
60
|
}, {
|
|
61
61
|
readonly threadID?: string | undefined;
|
|
62
|
-
readonly status?:
|
|
62
|
+
readonly status?: "blocked" | "cancelled" | "done" | "failed" | "in_progress" | "open" | "superseded" | undefined;
|
|
63
63
|
readonly limit?: number | undefined;
|
|
64
64
|
}> & Schema.Struct<{
|
|
65
65
|
readonly threadID: Schema.optionalKey<Schema.String>;
|
|
66
|
-
readonly status: Schema.optionalKey<Schema.
|
|
66
|
+
readonly status: Schema.optionalKey<Schema.Literals<readonly ["open", "in_progress", "blocked", "done", "failed", "cancelled", "superseded"]>>;
|
|
67
67
|
readonly limit: Schema.optionalKey<Schema.Number>;
|
|
68
68
|
}>;
|
|
69
69
|
output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
@@ -73,6 +73,15 @@ export declare const Definition: {
|
|
|
73
73
|
}> & Schema.Struct<{
|
|
74
74
|
readonly text: Schema.String;
|
|
75
75
|
}>;
|
|
76
|
+
errors: {
|
|
77
|
+
"tickets.invalid_status": import("effect/StandardSchema").StandardSchemaV1<{
|
|
78
|
+
readonly status: string;
|
|
79
|
+
}, {
|
|
80
|
+
readonly status: string;
|
|
81
|
+
}> & Schema.Struct<{
|
|
82
|
+
readonly status: Schema.String;
|
|
83
|
+
}>;
|
|
84
|
+
};
|
|
76
85
|
};
|
|
77
86
|
ticket: {
|
|
78
87
|
input: import("effect/StandardSchema").StandardSchemaV1<{
|
package/dist/src/protocol.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Rpc } from "@opencode/plugin/effect";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
|
+
// `ticket-view` carries the TUI-safe copy of the state list (it must not import
|
|
4
|
+
// the KV-backed store); `test/ticket-view.test.ts` keeps it in step with
|
|
5
|
+
// `src/ticket.ts`, so reusing it here keeps the RPC enum from drifting.
|
|
6
|
+
import { TICKET_STATES } from "./ticket-view.js";
|
|
3
7
|
/**
|
|
4
8
|
* Server-side RPC contract for the Super-agent P0 prototype.
|
|
5
9
|
*
|
|
@@ -32,10 +36,13 @@ export const Definition = Rpc.define({
|
|
|
32
36
|
tickets: {
|
|
33
37
|
input: Schema.toStandardSchemaV1(Schema.Struct({
|
|
34
38
|
threadID: Schema.optionalKey(Schema.String),
|
|
35
|
-
status: Schema.optionalKey(Schema.
|
|
39
|
+
status: Schema.optionalKey(Schema.Literals(TICKET_STATES)),
|
|
36
40
|
limit: Schema.optionalKey(Schema.Number),
|
|
37
41
|
})),
|
|
38
42
|
output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
|
|
43
|
+
errors: {
|
|
44
|
+
"tickets.invalid_status": Schema.toStandardSchemaV1(Schema.Struct({ status: Schema.String })),
|
|
45
|
+
},
|
|
39
46
|
},
|
|
40
47
|
ticket: {
|
|
41
48
|
input: Schema.toStandardSchemaV1(Schema.Struct({ ticketID: Schema.String })),
|
package/dist/src/report.d.ts
CHANGED
|
@@ -54,6 +54,12 @@ export declare function buildReport(sessionID: string, text: string, capturedAt?
|
|
|
54
54
|
export declare function reportSummary(record: ReportRecord): string;
|
|
55
55
|
/** One router-facing line that names the full-text handle. */
|
|
56
56
|
export declare function reportNotice(record: ReportRecord): string;
|
|
57
|
+
/**
|
|
58
|
+
* Last non-empty assistant text in a session-context snapshot. Shared by the
|
|
59
|
+
* live watcher and startup hydration so a crash-recovered worker gets the
|
|
60
|
+
* same report extraction as one that ended while the plugin was running.
|
|
61
|
+
*/
|
|
62
|
+
export declare function lastAssistantText(messages: ReadonlyArray<unknown>): string | undefined;
|
|
57
63
|
/** Page through a stored report; the caller validates offset against `stored`. */
|
|
58
64
|
export declare function sliceReport(record: ReportRecord, offset: number, limit: number): ReportSlice;
|
|
59
65
|
export declare function encodeReport(record: ReportRecord): Schema.Json;
|
package/dist/src/report.js
CHANGED
|
@@ -44,6 +44,35 @@ export function reportNotice(record) {
|
|
|
44
44
|
const clip = record.truncated ? `,超出 ${REPORT_TEXT_LIMIT} 字上限仅存前 ${record.text.length} 字` : "";
|
|
45
45
|
return `全文长度 ${record.length}${clip},可用 report_read 读取(sessionID: ${record.sessionID})`;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Last non-empty assistant text in a session-context snapshot. Shared by the
|
|
49
|
+
* live watcher and startup hydration so a crash-recovered worker gets the
|
|
50
|
+
* same report extraction as one that ended while the plugin was running.
|
|
51
|
+
*/
|
|
52
|
+
export function lastAssistantText(messages) {
|
|
53
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
54
|
+
const message = messages[index];
|
|
55
|
+
if (typeof message !== "object" || message === null)
|
|
56
|
+
continue;
|
|
57
|
+
const typed = message;
|
|
58
|
+
if (typed.type !== "assistant")
|
|
59
|
+
continue;
|
|
60
|
+
if (!Array.isArray(typed.content))
|
|
61
|
+
continue;
|
|
62
|
+
const text = typed.content
|
|
63
|
+
.flatMap((part) => {
|
|
64
|
+
if (typeof part !== "object" || part === null)
|
|
65
|
+
return [];
|
|
66
|
+
const candidate = part;
|
|
67
|
+
return candidate.type === "text" && typeof candidate.text === "string" ? [candidate.text] : [];
|
|
68
|
+
})
|
|
69
|
+
.join("\n")
|
|
70
|
+
.trim();
|
|
71
|
+
if (text !== "")
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
47
76
|
/** Page through a stored report; the caller validates offset against `stored`. */
|
|
48
77
|
export function sliceReport(record, offset, limit) {
|
|
49
78
|
const stored = record.text.length;
|
package/dist/src/router.d.ts
CHANGED
|
@@ -22,9 +22,8 @@ export declare const hydratePending: (ctx: Plugin.Context) => Effect.Effect<void
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const ensureRouter: (ctx: Plugin.Context) => Effect.Effect<string, unknown, never>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* failure they stay queued for the next attempt.
|
|
25
|
+
* Serialised entry point for every flush caller (idle hook, RPC, tests).
|
|
26
|
+
* Concurrent calls wait on `flushLock`, so one batch is delivered once.
|
|
28
27
|
*/
|
|
29
28
|
export declare const flushPending: (ctx: Plugin.Context) => Effect.Effect<void, unknown, never>;
|
|
30
29
|
/**
|
package/dist/src/router.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Agent } from "@opencode/plugin/effect";
|
|
2
|
-
import { Effect, Option } from "effect";
|
|
2
|
+
import { Effect, Option, Semaphore } from "effect";
|
|
3
3
|
import { ROUTER_AGENT } from "./agents.js";
|
|
4
4
|
import { threadDirHint } from "./dirs.js";
|
|
5
5
|
import { formatTrigger } from "./format.js";
|
|
@@ -8,6 +8,13 @@ import { ROUTER_KEY, store, toSessionID } from "./state.js";
|
|
|
8
8
|
import { noteRouterTrigger } from "./ticket.js";
|
|
9
9
|
import { ROUTER_PERMISSIONS } from "./tools.js";
|
|
10
10
|
const MAX_FLUSH = 20;
|
|
11
|
+
/**
|
|
12
|
+
* Serialise flushes. A second caller waits for the in-flight flush to finish,
|
|
13
|
+
* then observes the drained queue and only flushes events enqueued meanwhile,
|
|
14
|
+
* so the same batch is never injected twice. Effect releases the permit on
|
|
15
|
+
* success, failure and interruption.
|
|
16
|
+
*/
|
|
17
|
+
const flushLock = Semaphore.makeUnsafe(1);
|
|
11
18
|
/** KV prefix for crash-safe pending triggers. Owned by the router; `state.ts` stays untouched. */
|
|
12
19
|
export const PENDING_PREFIX = "pending:";
|
|
13
20
|
const pendingKey = (id) => `${PENDING_PREFIX}${id}`;
|
|
@@ -161,11 +168,15 @@ export const ensureRouter = Effect.fn("superagent.ensureRouter")(function* (ctx)
|
|
|
161
168
|
* leave the queue (memory and KV) only after the injection succeeds; on
|
|
162
169
|
* failure they stay queued for the next attempt.
|
|
163
170
|
*/
|
|
164
|
-
|
|
171
|
+
const flushPendingUnlocked = Effect.fn("superagent.flushPendingUnlocked")(function* (ctx) {
|
|
165
172
|
if (store.pending.length === 0)
|
|
166
173
|
return;
|
|
167
174
|
const routerID = yield* ensureRouter(ctx);
|
|
168
175
|
const batch = store.pending.slice(0, MAX_FLUSH);
|
|
176
|
+
// Identity snapshot of this delivery. Removal below matches these ids
|
|
177
|
+
// instead of the queue prefix, so events enqueued while the injection is
|
|
178
|
+
// in flight survive.
|
|
179
|
+
const batchIDs = new Set(batch.map((item) => pendingID(item.trigger, item.arrivedAt)));
|
|
169
180
|
const text = [
|
|
170
181
|
`${batch.length} new event(s) for the router:`,
|
|
171
182
|
...batch.map((item) => {
|
|
@@ -183,7 +194,14 @@ export const flushPending = Effect.fn("superagent.flushPending")(function* (ctx)
|
|
|
183
194
|
});
|
|
184
195
|
return;
|
|
185
196
|
}
|
|
186
|
-
|
|
197
|
+
// Remove by delivered id, never by position: the queue may have grown at
|
|
198
|
+
// the tail (or been trimmed elsewhere) while `synthetic` was awaited.
|
|
199
|
+
for (let index = store.pending.length - 1; index >= 0; index -= 1) {
|
|
200
|
+
const item = store.pending[index];
|
|
201
|
+
if (item !== undefined && batchIDs.has(pendingID(item.trigger, item.arrivedAt))) {
|
|
202
|
+
store.pending.splice(index, 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
187
205
|
// Remember what the Router was just told: `ticket_create` copies this
|
|
188
206
|
// trigger into the ticket's authorization (id + quote).
|
|
189
207
|
const newest = batch[batch.length - 1];
|
|
@@ -193,6 +211,13 @@ export const flushPending = Effect.fn("superagent.flushPending")(function* (ctx)
|
|
|
193
211
|
yield* ctx.storage.remove(keyOf(item)).pipe(Effect.catch(() => Effect.void));
|
|
194
212
|
}
|
|
195
213
|
});
|
|
214
|
+
/**
|
|
215
|
+
* Serialised entry point for every flush caller (idle hook, RPC, tests).
|
|
216
|
+
* Concurrent calls wait on `flushLock`, so one batch is delivered once.
|
|
217
|
+
*/
|
|
218
|
+
export const flushPending = Effect.fn("superagent.flushPending")(function* (ctx) {
|
|
219
|
+
yield* flushLock.withPermit(flushPendingUnlocked(ctx));
|
|
220
|
+
});
|
|
196
221
|
/**
|
|
197
222
|
* Router idle hook: when the router goes idle with nothing pending in the
|
|
198
223
|
* background, flush queued triggers into the same execution. An empty queue
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/effect";
|
|
2
|
+
import { Tool } from "@opencode/schema/tool";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
/**
|
|
5
|
+
* Router-only `sleep` tool: a bounded, real pause after `dispatch` so the
|
|
6
|
+
* Worker gets execution time before the Router decides to act again. It is a
|
|
7
|
+
* timer, never a busy loop, so the Router session (and every other fiber in
|
|
8
|
+
* the process) stays responsive while it waits.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SLEEP_DEFAULT_SECONDS = 30;
|
|
11
|
+
export declare const SLEEP_MIN_SECONDS = 1;
|
|
12
|
+
export declare const SLEEP_MAX_SECONDS = 600;
|
|
13
|
+
export declare const SleepInput: Schema.Struct<{
|
|
14
|
+
readonly seconds: Schema.optionalKey<Schema.Number>;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const SleepOutput: Schema.Struct<{
|
|
17
|
+
readonly slept: Schema.Number;
|
|
18
|
+
readonly text: Schema.String;
|
|
19
|
+
}>;
|
|
20
|
+
interface SleepInput {
|
|
21
|
+
readonly seconds?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Performs the wait; tests inject a no-op so the default 30s never runs. */
|
|
24
|
+
export type Sleeper = (seconds: number) => Effect.Effect<void>;
|
|
25
|
+
/** The production sleeper: a platform timer, not a spin loop. */
|
|
26
|
+
export declare const effectSleeper: Sleeper;
|
|
27
|
+
export declare const runSleep: (input: SleepInput, caller: string, sleeper?: Sleeper | undefined) => Effect.Effect<{
|
|
28
|
+
output: {
|
|
29
|
+
slept: number;
|
|
30
|
+
text: string;
|
|
31
|
+
};
|
|
32
|
+
content: string;
|
|
33
|
+
}, Tool.Error, never>;
|
|
34
|
+
export declare const registerSleep: (ctx: Plugin.Context) => Effect.Effect<void, never, import("effect/Scope").Scope>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Tool } from "@opencode/schema/tool";
|
|
2
|
+
import { Duration, Effect, Schema } from "effect";
|
|
3
|
+
import { requireRouter } from "./guard.js";
|
|
4
|
+
import { specFor, toolPermission } from "./tools.js";
|
|
5
|
+
/**
|
|
6
|
+
* Router-only `sleep` tool: a bounded, real pause after `dispatch` so the
|
|
7
|
+
* Worker gets execution time before the Router decides to act again. It is a
|
|
8
|
+
* timer, never a busy loop, so the Router session (and every other fiber in
|
|
9
|
+
* the process) stays responsive while it waits.
|
|
10
|
+
*/
|
|
11
|
+
export const SLEEP_DEFAULT_SECONDS = 30;
|
|
12
|
+
export const SLEEP_MIN_SECONDS = 1;
|
|
13
|
+
export const SLEEP_MAX_SECONDS = 600;
|
|
14
|
+
export const SleepInput = Schema.Struct({
|
|
15
|
+
seconds: Schema.optionalKey(Schema.Number),
|
|
16
|
+
});
|
|
17
|
+
export const SleepOutput = Schema.Struct({
|
|
18
|
+
slept: Schema.Number,
|
|
19
|
+
text: Schema.String,
|
|
20
|
+
});
|
|
21
|
+
/** The production sleeper: a platform timer, not a spin loop. */
|
|
22
|
+
export const effectSleeper = (seconds) => Effect.sleep(Duration.seconds(seconds));
|
|
23
|
+
export const runSleep = Effect.fn("superagent.sleep")(function* (input, caller, sleeper = effectSleeper) {
|
|
24
|
+
yield* requireRouter(caller, "sleep");
|
|
25
|
+
const seconds = input.seconds ?? SLEEP_DEFAULT_SECONDS;
|
|
26
|
+
// The registered schema already rejects non-numbers at the tool boundary;
|
|
27
|
+
// this re-check keeps direct/RPC-style callers from bypassing validation.
|
|
28
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) {
|
|
29
|
+
return yield* new Tool.Error({
|
|
30
|
+
message: `sleep seconds must be a number between ${SLEEP_MIN_SECONDS} and ${SLEEP_MAX_SECONDS} (got ${String(input.seconds)})`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isInteger(seconds)) {
|
|
34
|
+
return yield* new Tool.Error({
|
|
35
|
+
message: `sleep seconds must be an integer between ${SLEEP_MIN_SECONDS} and ${SLEEP_MAX_SECONDS} (got ${seconds})`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (seconds < SLEEP_MIN_SECONDS || seconds > SLEEP_MAX_SECONDS) {
|
|
39
|
+
return yield* new Tool.Error({
|
|
40
|
+
message: `sleep seconds must be between ${SLEEP_MIN_SECONDS} and ${SLEEP_MAX_SECONDS} (got ${seconds})`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
yield* sleeper(seconds);
|
|
44
|
+
const text = `slept ${seconds}s`;
|
|
45
|
+
return { output: { slept: seconds, text }, content: text };
|
|
46
|
+
});
|
|
47
|
+
export const registerSleep = Effect.fn("superagent.registerSleep")(function* (ctx) {
|
|
48
|
+
yield* ctx.tool.transform((editor) => {
|
|
49
|
+
editor.add({
|
|
50
|
+
name: "sleep",
|
|
51
|
+
description: specFor("sleep").description,
|
|
52
|
+
input: SleepInput,
|
|
53
|
+
output: SleepOutput,
|
|
54
|
+
options: { permission: toolPermission(specFor("sleep")) },
|
|
55
|
+
execute: (input, context) => runSleep(input, context.sessionID),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
package/dist/src/state.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export declare const threadKey: (threadID: string) => string;
|
|
|
99
99
|
export declare const triggerKey: (id: string) => string;
|
|
100
100
|
/** Brands are compile-time only, so a validated session id string converts freely. */
|
|
101
101
|
export declare const toSessionID: (id: string) => Session.ID;
|
|
102
|
+
/** A Session is running while `time.busy` is newer than the last completed execution's `time.idle`. */
|
|
103
|
+
export declare function isRunning(info: Session.Info): boolean;
|
|
102
104
|
export declare function decodeWorker(value: Schema.Json | undefined): WorkerRecord | undefined;
|
|
103
105
|
export declare function decodeThread(value: Schema.Json | undefined): ThreadRecord | undefined;
|
|
104
106
|
export declare function decodeTrigger(value: Schema.Json | undefined): TriggerRecord | undefined;
|
package/dist/src/state.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { DateTime, Effect } from "effect";
|
|
2
|
+
import { buildReport, lastAssistantText, reportSummary, saveReport } from "./report.js";
|
|
3
|
+
import { closeTicketsForWorker } from "./ticket.js";
|
|
2
4
|
const ROUTES = ["router", "worker"];
|
|
3
5
|
/** Host context for fire-and-forget KV writes from synchronous interception points. Set during hydrate. */
|
|
4
6
|
let persistHost;
|
|
@@ -120,6 +122,14 @@ export const threadKey = (threadID) => `${THREAD_PREFIX}${threadID}`;
|
|
|
120
122
|
export const triggerKey = (id) => `${TRIGGER_PREFIX}${id}`;
|
|
121
123
|
/** Brands are compile-time only, so a validated session id string converts freely. */
|
|
122
124
|
export const toSessionID = (id) => id;
|
|
125
|
+
/** A Session is running while `time.busy` is newer than the last completed execution's `time.idle`. */
|
|
126
|
+
export function isRunning(info) {
|
|
127
|
+
const busy = info.time.busy;
|
|
128
|
+
if (busy === undefined)
|
|
129
|
+
return false;
|
|
130
|
+
const idle = info.time.idle;
|
|
131
|
+
return idle === undefined || DateTime.toEpochMillis(busy) > DateTime.toEpochMillis(idle);
|
|
132
|
+
}
|
|
123
133
|
const STATES = ["running", "idle", "failed", "done", "aborted"];
|
|
124
134
|
function isRecord(value) {
|
|
125
135
|
return typeof value === "object" && value !== null;
|
|
@@ -260,6 +270,43 @@ const removeWorker = Effect.fn("superagent.removeWorker")(function* (ctx, sessio
|
|
|
260
270
|
.remove(workerKey(sessionID))
|
|
261
271
|
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to remove worker", { cause })));
|
|
262
272
|
});
|
|
273
|
+
/**
|
|
274
|
+
* Reconcile one hydrated `running` record with the live session. A session
|
|
275
|
+
* that is still executing keeps `running`; one that finished while the plugin
|
|
276
|
+
* was down is backfilled the same way the watcher would have handled it:
|
|
277
|
+
* capture the last assistant report, persist the terminal/interrupted state,
|
|
278
|
+
* and close its ticket for `done`/`failed`.
|
|
279
|
+
*/
|
|
280
|
+
const reconcileRunningWorker = Effect.fn("superagent.reconcileRunningWorker")(function* (ctx, record, info) {
|
|
281
|
+
if (isRunning(info))
|
|
282
|
+
return;
|
|
283
|
+
const state = info.outcome === "succeeded" ? "done" : info.outcome === "failed" ? "failed" : "idle";
|
|
284
|
+
record.state = state;
|
|
285
|
+
const sessions = ctx.session;
|
|
286
|
+
const messages = typeof sessions.context === "function"
|
|
287
|
+
? yield* sessions
|
|
288
|
+
.context({ sessionID: toSessionID(record.sessionID) })
|
|
289
|
+
.pipe(Effect.catch(() => Effect.succeed([])))
|
|
290
|
+
: [];
|
|
291
|
+
const report = lastAssistantText(messages);
|
|
292
|
+
if (report !== undefined) {
|
|
293
|
+
const captured = buildReport(record.sessionID, report);
|
|
294
|
+
yield* saveReport(ctx, captured);
|
|
295
|
+
record.lastReport = reportSummary(captured);
|
|
296
|
+
}
|
|
297
|
+
yield* saveWorker(ctx, record);
|
|
298
|
+
if (state === "done" || state === "failed") {
|
|
299
|
+
yield* closeTicketsForWorker(ctx, {
|
|
300
|
+
sessionID: record.sessionID,
|
|
301
|
+
outcome: state,
|
|
302
|
+
summary: record.lastReport ?? report ?? `${state} (worker ${record.sessionID})`,
|
|
303
|
+
}).pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to close ticket for rehydrated worker", {
|
|
304
|
+
sessionID: record.sessionID,
|
|
305
|
+
cause,
|
|
306
|
+
})));
|
|
307
|
+
}
|
|
308
|
+
yield* Effect.logInfo("superagent rehydrated ended worker", { sessionID: record.sessionID, state });
|
|
309
|
+
});
|
|
263
310
|
/**
|
|
264
311
|
* Load KV state into the in-memory maps. Storage is the unique source of
|
|
265
312
|
* truth; every loaded session is validated with `session.get` and stale
|
|
@@ -296,6 +343,8 @@ export const hydrate = Effect.fn("superagent.hydrate")(function* (ctx) {
|
|
|
296
343
|
continue;
|
|
297
344
|
}
|
|
298
345
|
store.workers.set(record.sessionID, record);
|
|
346
|
+
if (record.state === "running")
|
|
347
|
+
yield* reconcileRunningWorker(ctx, record, info);
|
|
299
348
|
}
|
|
300
349
|
after = page.next;
|
|
301
350
|
} while (after !== undefined);
|
package/dist/src/ticket.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 TriggerInput } from "./state.js";
|
|
4
|
+
import { type TriggerInput, type WorkerState } from "./state.js";
|
|
5
5
|
/**
|
|
6
6
|
* Ticket (work order) store for the Super-agent P0.
|
|
7
7
|
*
|
|
@@ -142,6 +142,13 @@ export declare function ticketStats(): TicketStats;
|
|
|
142
142
|
export declare function activeTicketSummary(threadID: string | undefined, limit?: number): string;
|
|
143
143
|
/** Active ticket for a worker session, used when a worker ends. */
|
|
144
144
|
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;
|
|
145
152
|
export declare function validateCreateInput(input: {
|
|
146
153
|
readonly threadID?: string;
|
|
147
154
|
readonly title?: string;
|
|
@@ -209,7 +216,7 @@ export declare function listTickets(input?: ListTicketsInput): Array<Ticket>;
|
|
|
209
216
|
* Gate every dispatch must pass: a dispatch always names a ticket, and that
|
|
210
217
|
* ticket must be workable right now. The rules are enforced here (not in the
|
|
211
218
|
* prompt) so a Router cannot dispatch without a ticket, onto a closed ticket,
|
|
212
|
-
* across threads, or onto a ticket that already has a
|
|
219
|
+
* across threads, or onto a ticket that already has a live (non-terminal) worker.
|
|
213
220
|
*/
|
|
214
221
|
export declare const requireDispatchTicket: (ticketID: string | undefined, threadID: string | undefined) => Effect.Effect<Ticket, Tool.Error, never>;
|
|
215
222
|
export interface LinkTicketInput {
|