opencode-superagent 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -10
- package/dist/server.d.ts +237 -0
- package/dist/server.js +56 -24
- package/dist/src/abort.js +8 -13
- package/dist/src/context.d.ts +10 -3
- package/dist/src/context.js +24 -8
- package/dist/src/dispatch.d.ts +6 -0
- package/dist/src/dispatch.js +100 -11
- package/dist/src/prompts.d.ts +1 -1
- package/dist/src/prompts.js +2 -1
- package/dist/src/protocol.d.ts +24 -7
- package/dist/src/protocol.js +24 -6
- package/dist/src/report.d.ts +6 -0
- package/dist/src/report.js +29 -0
- package/dist/src/router.d.ts +2 -3
- package/dist/src/router.js +28 -3
- package/dist/src/sleep.d.ts +35 -0
- package/dist/src/sleep.js +58 -0
- package/dist/src/state.d.ts +4 -21
- package/dist/src/state.js +55 -4
- package/dist/src/ticket-state.d.ts +97 -0
- package/dist/src/ticket-state.js +70 -0
- package/dist/src/ticket.d.ts +20 -60
- package/dist/src/ticket.js +40 -49
- package/dist/src/tools.js +21 -2
- package/dist/src/version.d.ts +28 -0
- package/dist/src/version.js +76 -0
- package/dist/src/watchdog.d.ts +11 -4
- package/dist/src/watchdog.js +37 -20
- package/dist/src/watcher.d.ts +6 -0
- package/dist/src/watcher.js +19 -33
- package/dist/src/worker-state.d.ts +41 -0
- package/dist/src/worker-state.js +22 -0
- package/dist/tui.js +113 -78
- package/package.json +3 -1
package/dist/src/dispatch.js
CHANGED
|
@@ -6,8 +6,9 @@ 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
|
+
import { pluginVersion } from "./version.js";
|
|
11
12
|
export const DispatchInput = Schema.Struct({
|
|
12
13
|
task: Schema.String,
|
|
13
14
|
title: Schema.optionalKey(Schema.String),
|
|
@@ -18,6 +19,24 @@ export const DispatchInput = Schema.Struct({
|
|
|
18
19
|
});
|
|
19
20
|
const StatusOutput = Schema.Struct({ text: Schema.String });
|
|
20
21
|
const DispatchOutput = Schema.Struct({ sessionID: Schema.String });
|
|
22
|
+
/**
|
|
23
|
+
* Tickets with a dispatch currently in flight in this process. `runDispatch`
|
|
24
|
+
* checks and inserts in one synchronous block, so two concurrent dispatches of
|
|
25
|
+
* the same ticket cannot both pass: the loser fails with an explicit error
|
|
26
|
+
* instead of creating a second worker session. The claim is released on
|
|
27
|
+
* success, failure and interruption via `Effect.ensuring`.
|
|
28
|
+
*/
|
|
29
|
+
const inFlightTickets = new Set();
|
|
30
|
+
/** Synchronous check-and-claim: only the first caller for a ticket gets `true`. */
|
|
31
|
+
function claimDispatch(ticketID) {
|
|
32
|
+
if (inFlightTickets.has(ticketID))
|
|
33
|
+
return false;
|
|
34
|
+
inFlightTickets.add(ticketID);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function releaseDispatch(ticketID) {
|
|
38
|
+
inFlightTickets.delete(ticketID);
|
|
39
|
+
}
|
|
21
40
|
/** The ticket header a worker sees first: objective and acceptance, verbatim. */
|
|
22
41
|
function ticketHeader(ticket) {
|
|
23
42
|
const acceptance = ticket.acceptance.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
@@ -27,10 +46,16 @@ function workerPrompt(threadID, title, task, ticket) {
|
|
|
27
46
|
const subject = title === undefined ? "" : `Title: ${title}\n`;
|
|
28
47
|
return `${ticketHeader(ticket)}Thread: ${threadID}\n${subject}Task:\n${task}\n\nEnd with a short report: what changed, how it was verified, remaining risks.`;
|
|
29
48
|
}
|
|
30
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Compact status text for the `status` tool and its diagnostics readers.
|
|
51
|
+
* Starts with the plugin version so a stale gateway (code upgraded but not
|
|
52
|
+
* restarted) is visible from the first line instead of guessed from behavior.
|
|
53
|
+
*/
|
|
54
|
+
export function statusText() {
|
|
31
55
|
const stats = ticketStats();
|
|
32
56
|
const active = activeTicketSummary(undefined, 5);
|
|
33
57
|
const lines = [
|
|
58
|
+
`superagent ${pluginVersion()} — restart the gateway after upgrading (no hot reload)`,
|
|
34
59
|
`router: ${store.routerID ?? "none"} pending: ${store.pending.length} workers: ${store.workers.size} tickets: open=${stats.open} in_progress=${stats.inProgress} blocked=${stats.blocked} closed=${stats.closed}`,
|
|
35
60
|
];
|
|
36
61
|
if (active !== "")
|
|
@@ -45,8 +70,48 @@ function statusText() {
|
|
|
45
70
|
}
|
|
46
71
|
return lines.join("\n");
|
|
47
72
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Undo a half-created dispatch: fail the ticket bound to the worker, detach
|
|
75
|
+
* the worker from its thread affinity lists, forget the worker record and stop
|
|
76
|
+
* its session. Every step is best-effort after the first failure, so one
|
|
77
|
+
* storage problem cannot leave the whole dispatch half-rolled-back.
|
|
78
|
+
*/
|
|
79
|
+
const rollbackDispatch = Effect.fn("superagent.rollbackDispatch")(function* (ctx, sessionID, reason) {
|
|
80
|
+
// Fail the ticket while the worker → ticket edge still exists, so the
|
|
81
|
+
// Router sees why the dispatch never started instead of a stuck in_progress.
|
|
82
|
+
const ticket = activeTicketForWorker(sessionID);
|
|
83
|
+
if (ticket !== undefined) {
|
|
84
|
+
yield* closeTicket(ctx, {
|
|
85
|
+
ticketID: ticket.id,
|
|
86
|
+
outcome: "failed",
|
|
87
|
+
summary: `dispatch rollback: ${reason}`,
|
|
88
|
+
}).pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to fail ticket for rolled-back worker", {
|
|
89
|
+
sessionID,
|
|
90
|
+
ticketID: ticket.id,
|
|
91
|
+
cause,
|
|
92
|
+
})));
|
|
93
|
+
}
|
|
94
|
+
// Thread affinity is written before the prompt; drop the worker from every
|
|
95
|
+
// list it was added to so the thread falls back to the Router.
|
|
96
|
+
const worker = store.workers.get(sessionID);
|
|
97
|
+
if (worker !== undefined) {
|
|
98
|
+
for (const threadID of worker.threads) {
|
|
99
|
+
const current = store.threads.get(threadID);
|
|
100
|
+
if (current === undefined || !current.includes(sessionID))
|
|
101
|
+
continue;
|
|
102
|
+
const next = current.filter((id) => id !== sessionID);
|
|
103
|
+
if (next.length === 0) {
|
|
104
|
+
store.threads.delete(threadID);
|
|
105
|
+
yield* ctx.storage.remove(threadKey(threadID)).pipe(Effect.catch(() => Effect.void));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
store.threads.set(threadID, next);
|
|
109
|
+
yield* ctx.storage
|
|
110
|
+
.set(threadKey(threadID), encodeThread({ threadID, workers: next, updatedAt: Date.now() }))
|
|
111
|
+
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist thread", { cause })));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
50
115
|
store.workers.delete(sessionID);
|
|
51
116
|
yield* ctx.storage
|
|
52
117
|
.remove(workerKey(sessionID))
|
|
@@ -71,16 +136,32 @@ export const runDispatch = Effect.fn("superagent.dispatch")(function* (ctx, inpu
|
|
|
71
136
|
return yield* new Tool.Error({ message: "dispatch is only available to the router session" });
|
|
72
137
|
}
|
|
73
138
|
const routerID = store.routerID;
|
|
139
|
+
// Every dispatch is backed by a ticket: the gate refuses a missing ticket,
|
|
140
|
+
// an unknown/closed one, a thread mismatch and a ticket whose worker is
|
|
141
|
+
// still running, so no dispatch can bypass the work-order record.
|
|
142
|
+
const ticket = yield* requireDispatchTicket(input.ticketID, input.threadID);
|
|
143
|
+
// Claim the ticket synchronously before any further dispatch step. Check
|
|
144
|
+
// and insert happen in one uninterrupted block, so two concurrent calls can
|
|
145
|
+
// never both see the ticket as free and race into `sessions.create`; the
|
|
146
|
+
// loser gets an explicit error instead of a second worker.
|
|
147
|
+
if (!claimDispatch(ticket.id)) {
|
|
148
|
+
return yield* new Tool.Error({ message: `dispatch already in progress for ticket ${ticket.id}` });
|
|
149
|
+
}
|
|
150
|
+
return yield* completeDispatch(ctx, input, routerID, ticket).pipe(
|
|
151
|
+
// Success, failure and interruption all release the claim.
|
|
152
|
+
Effect.ensuring(Effect.sync(() => releaseDispatch(ticket.id))));
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Body of a dispatch that already owns the ticket claim. `runDispatch` is the
|
|
156
|
+
* only caller and releases the claim on every exit path.
|
|
157
|
+
*/
|
|
158
|
+
const completeDispatch = Effect.fn("superagent.dispatchClaimed")(function* (ctx, input, routerID, ticket) {
|
|
74
159
|
const agents = ctx.agent;
|
|
75
160
|
const sessions = ctx.session;
|
|
76
161
|
const requested = input.agent ?? WORKER_AGENT;
|
|
77
162
|
if (input.directory !== undefined && normalizeDirectory(input.directory) === undefined) {
|
|
78
163
|
return yield* new Tool.Error({ message: "dispatch directory must be an absolute path, e.g. /opt/ai-gw" });
|
|
79
164
|
}
|
|
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
165
|
// The ticket carries its thread; the dispatch must agree with it and
|
|
85
166
|
// inherits it when the call omits one.
|
|
86
167
|
const threadID = input.threadID ?? ticket.threadID;
|
|
@@ -162,7 +243,7 @@ export const runDispatch = Effect.fn("superagent.dispatch")(function* (ctx, inpu
|
|
|
162
243
|
...(targetDirectory === undefined ? {} : { note: targetDirectory }),
|
|
163
244
|
}).pipe(Effect.as(undefined), Effect.catchTag("Tool.Error", (error) => Effect.succeed(error.message)));
|
|
164
245
|
if (linkFailure !== undefined) {
|
|
165
|
-
yield* rollbackDispatch(ctx, created.id);
|
|
246
|
+
yield* rollbackDispatch(ctx, created.id, `could not bind ticket ${ticket.id} to the new worker (${linkFailure})`);
|
|
166
247
|
return yield* new Tool.Error({
|
|
167
248
|
message: `dispatch aborted: could not bind ticket ${ticket.id} to the new worker (${linkFailure})`,
|
|
168
249
|
});
|
|
@@ -178,13 +259,21 @@ export const runDispatch = Effect.fn("superagent.dispatch")(function* (ctx, inpu
|
|
|
178
259
|
yield* ctx.storage
|
|
179
260
|
.set(threadKey(threadID), encodeThread({ threadID, workers: [...existing], updatedAt: Date.now() }))
|
|
180
261
|
.pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist thread", { cause })));
|
|
181
|
-
|
|
262
|
+
// Prompt delivery is part of the dispatch: if the worker never receives its
|
|
263
|
+
// task, the ticket, thread affinity and worker record must not stay behind.
|
|
264
|
+
const promptFailure = yield* sessions
|
|
182
265
|
.prompt({
|
|
183
266
|
sessionID: created.id,
|
|
184
267
|
text: workerPrompt(threadID, input.title, input.task, ticket),
|
|
185
268
|
delivery: "queue",
|
|
186
269
|
})
|
|
187
|
-
.pipe(Effect.
|
|
270
|
+
.pipe(Effect.as(undefined), Effect.catch((cause) => Effect.succeed(String(cause))));
|
|
271
|
+
if (promptFailure !== undefined) {
|
|
272
|
+
yield* rollbackDispatch(ctx, created.id, `failed to prompt worker session: ${promptFailure}`);
|
|
273
|
+
return yield* new Tool.Error({
|
|
274
|
+
message: `dispatch aborted: failed to prompt worker session: ${promptFailure}`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
188
277
|
const where = targetDirectory === undefined ? "gateway default directory" : targetDirectory;
|
|
189
278
|
return {
|
|
190
279
|
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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The single RPC contract shared by the server and the TUI.
|
|
4
4
|
*
|
|
5
5
|
* The TUI (`tui.tsx`) calls these methods through `client.rpc(Definition)`,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* and `server.ts` registers the very same object (`ServerDefinition =
|
|
7
|
+
* Definition`), so method names, input/output schemas and declared errors
|
|
8
|
+
* cannot drift between the two halves. Schemas are wrapped with
|
|
9
|
+
* `Schema.toStandardSchemaV1` exactly like `Goal.Definition`, which is what
|
|
10
|
+
* makes the same definition usable from both the Effect server
|
|
9
11
|
* (`ctx.rpc.register`) and the Promise TUI client.
|
|
10
12
|
*/
|
|
11
13
|
export declare const Definition: {
|
|
@@ -24,6 +26,7 @@ export declare const Definition: {
|
|
|
24
26
|
status: {
|
|
25
27
|
input: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
|
|
26
28
|
output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
29
|
+
readonly version?: string | undefined;
|
|
27
30
|
readonly router?: string | undefined;
|
|
28
31
|
readonly pending: number;
|
|
29
32
|
readonly workers: readonly {
|
|
@@ -33,6 +36,7 @@ export declare const Definition: {
|
|
|
33
36
|
readonly state: "aborted" | "done" | "failed" | "idle" | "running";
|
|
34
37
|
}[];
|
|
35
38
|
}, {
|
|
39
|
+
readonly version?: string | undefined;
|
|
36
40
|
readonly router?: string | undefined;
|
|
37
41
|
readonly pending: number;
|
|
38
42
|
readonly workers: readonly {
|
|
@@ -42,6 +46,7 @@ export declare const Definition: {
|
|
|
42
46
|
readonly state: "aborted" | "done" | "failed" | "idle" | "running";
|
|
43
47
|
}[];
|
|
44
48
|
}> & Schema.Struct<{
|
|
49
|
+
readonly version: Schema.optionalKey<Schema.String>;
|
|
45
50
|
readonly router: Schema.optionalKey<Schema.String>;
|
|
46
51
|
readonly pending: Schema.Number;
|
|
47
52
|
readonly workers: Schema.$Array<Schema.Struct<{
|
|
@@ -55,15 +60,15 @@ export declare const Definition: {
|
|
|
55
60
|
tickets: {
|
|
56
61
|
input: import("effect/StandardSchema").StandardSchemaV1<{
|
|
57
62
|
readonly threadID?: string | undefined;
|
|
58
|
-
readonly status?:
|
|
63
|
+
readonly status?: "blocked" | "cancelled" | "done" | "failed" | "in_progress" | "open" | "superseded" | undefined;
|
|
59
64
|
readonly limit?: number | undefined;
|
|
60
65
|
}, {
|
|
61
66
|
readonly threadID?: string | undefined;
|
|
62
|
-
readonly status?:
|
|
67
|
+
readonly status?: "blocked" | "cancelled" | "done" | "failed" | "in_progress" | "open" | "superseded" | undefined;
|
|
63
68
|
readonly limit?: number | undefined;
|
|
64
69
|
}> & Schema.Struct<{
|
|
65
70
|
readonly threadID: Schema.optionalKey<Schema.String>;
|
|
66
|
-
readonly status: Schema.optionalKey<Schema.
|
|
71
|
+
readonly status: Schema.optionalKey<Schema.Literals<readonly ["open", "in_progress", "blocked", "done", "failed", "cancelled", "superseded"]>>;
|
|
67
72
|
readonly limit: Schema.optionalKey<Schema.Number>;
|
|
68
73
|
}>;
|
|
69
74
|
output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
@@ -73,6 +78,15 @@ export declare const Definition: {
|
|
|
73
78
|
}> & Schema.Struct<{
|
|
74
79
|
readonly text: Schema.String;
|
|
75
80
|
}>;
|
|
81
|
+
errors: {
|
|
82
|
+
"tickets.invalid_status": import("effect/StandardSchema").StandardSchemaV1<{
|
|
83
|
+
readonly status: string;
|
|
84
|
+
}, {
|
|
85
|
+
readonly status: string;
|
|
86
|
+
}> & Schema.Struct<{
|
|
87
|
+
readonly status: Schema.String;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
76
90
|
};
|
|
77
91
|
ticket: {
|
|
78
92
|
input: import("effect/StandardSchema").StandardSchemaV1<{
|
|
@@ -176,6 +190,9 @@ export declare const Definition: {
|
|
|
176
190
|
}> & Schema.Struct<{
|
|
177
191
|
readonly interrupted: Schema.Boolean;
|
|
178
192
|
}>;
|
|
193
|
+
errors: {
|
|
194
|
+
"stop.rejected": import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
|
|
195
|
+
};
|
|
179
196
|
};
|
|
180
197
|
abort: {
|
|
181
198
|
input: import("effect/StandardSchema").StandardSchemaV1<{
|
package/dist/src/protocol.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
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
|
+
* The single RPC contract shared by the server and the TUI.
|
|
5
9
|
*
|
|
6
10
|
* The TUI (`tui.tsx`) calls these methods through `client.rpc(Definition)`,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* and `server.ts` registers the very same object (`ServerDefinition =
|
|
12
|
+
* Definition`), so method names, input/output schemas and declared errors
|
|
13
|
+
* cannot drift between the two halves. Schemas are wrapped with
|
|
14
|
+
* `Schema.toStandardSchemaV1` exactly like `Goal.Definition`, which is what
|
|
15
|
+
* makes the same definition usable from both the Effect server
|
|
10
16
|
* (`ctx.rpc.register`) and the Promise TUI client.
|
|
11
17
|
*/
|
|
12
18
|
export const Definition = Rpc.define({
|
|
@@ -19,6 +25,9 @@ export const Definition = Rpc.define({
|
|
|
19
25
|
status: {
|
|
20
26
|
input: Schema.toStandardSchemaV1(Schema.Struct({})),
|
|
21
27
|
output: Schema.toStandardSchemaV1(Schema.Struct({
|
|
28
|
+
// Optional so an older server that predates the version notice still
|
|
29
|
+
// satisfies the contract; the current server always sends it.
|
|
30
|
+
version: Schema.optionalKey(Schema.String),
|
|
22
31
|
router: Schema.optionalKey(Schema.String),
|
|
23
32
|
pending: Schema.Number,
|
|
24
33
|
workers: Schema.Array(Schema.Struct({
|
|
@@ -32,10 +41,13 @@ export const Definition = Rpc.define({
|
|
|
32
41
|
tickets: {
|
|
33
42
|
input: Schema.toStandardSchemaV1(Schema.Struct({
|
|
34
43
|
threadID: Schema.optionalKey(Schema.String),
|
|
35
|
-
status: Schema.optionalKey(Schema.
|
|
44
|
+
status: Schema.optionalKey(Schema.Literals(TICKET_STATES)),
|
|
36
45
|
limit: Schema.optionalKey(Schema.Number),
|
|
37
46
|
})),
|
|
38
47
|
output: Schema.toStandardSchemaV1(Schema.Struct({ text: Schema.String })),
|
|
48
|
+
errors: {
|
|
49
|
+
"tickets.invalid_status": Schema.toStandardSchemaV1(Schema.Struct({ status: Schema.String })),
|
|
50
|
+
},
|
|
39
51
|
},
|
|
40
52
|
ticket: {
|
|
41
53
|
input: Schema.toStandardSchemaV1(Schema.Struct({ ticketID: Schema.String })),
|
|
@@ -48,7 +60,9 @@ export const Definition = Rpc.define({
|
|
|
48
60
|
threadID: Schema.optionalKey(Schema.String),
|
|
49
61
|
agent: Schema.optionalKey(Schema.String),
|
|
50
62
|
directory: Schema.optionalKey(Schema.String),
|
|
51
|
-
// Required
|
|
63
|
+
// Required by the shape, but the RPC handler never runs it: RPC
|
|
64
|
+
// cannot prove router identity, so `dispatch` fails closed with
|
|
65
|
+
// `dispatch.rejected` and the router uses its tool instead.
|
|
52
66
|
ticketID: Schema.String,
|
|
53
67
|
})),
|
|
54
68
|
output: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
|
|
@@ -72,6 +86,10 @@ export const Definition = Rpc.define({
|
|
|
72
86
|
stop: {
|
|
73
87
|
input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String })),
|
|
74
88
|
output: Schema.toStandardSchemaV1(Schema.Struct({ interrupted: Schema.Boolean })),
|
|
89
|
+
// Declared in the shared contract, not only on the server: the TUI and
|
|
90
|
+
// the server register the same `Definition`, so a client can tell that
|
|
91
|
+
// `stop` is router-only without importing server internals.
|
|
92
|
+
errors: { "stop.rejected": Schema.toStandardSchemaV1(Schema.Struct({})) },
|
|
75
93
|
},
|
|
76
94
|
abort: {
|
|
77
95
|
input: Schema.toStandardSchemaV1(Schema.Struct({
|
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
|
+
});
|