@frockbot/plugin-shell 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frockbot.json +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
// The Shell's half of subagent dispatch: the seams between the parent Bot
|
|
3
|
+
// Durable Object, which is the authority, and the Subagent Durable Object,
|
|
4
|
+
// which is only an execution host (ADR 0017).
|
|
5
|
+
//
|
|
6
|
+
// Nothing here decides policy. The bounds and the records are the Subagents
|
|
7
|
+
// Package's; the Composition, the model binding and the admitted Turn are the
|
|
8
|
+
// Shell's; the Durable Object addressing is a binding this module is handed.
|
|
9
|
+
// What this module owns is the *order*: intent before effect, one settle per
|
|
10
|
+
// task, and a deadline that belongs to the parent's one alarm.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
decodeTaskOutcomeV1,
|
|
14
|
+
isTaskIdV1,
|
|
15
|
+
subagentExactKeys,
|
|
16
|
+
subagentText,
|
|
17
|
+
subagentTimestamp,
|
|
18
|
+
SubagentDecodeError,
|
|
19
|
+
TASK_MESSAGE_MAX_V1,
|
|
20
|
+
TASK_PROMPT_MAX_BYTES_V1,
|
|
21
|
+
TASK_TYPES_V1,
|
|
22
|
+
decodeTaskModelV1,
|
|
23
|
+
utf8ByteLengthV1,
|
|
24
|
+
type TaskModelV1,
|
|
25
|
+
type TaskOutcomeV1,
|
|
26
|
+
type TaskTypeV1,
|
|
27
|
+
} from "@frockbot/plugin-subagents/records";
|
|
28
|
+
import {
|
|
29
|
+
subagentDurableObjectNameV1,
|
|
30
|
+
taskSessionIdV1,
|
|
31
|
+
} from "@frockbot/plugin-subagents/storage-keys";
|
|
32
|
+
import type { BotIdentity } from "@frockbot/kernel-do";
|
|
33
|
+
|
|
34
|
+
/** The parent Turn that dispatched a task, as the child records it. */
|
|
35
|
+
export interface SubagentParentV1 {
|
|
36
|
+
userId: string;
|
|
37
|
+
botId: string;
|
|
38
|
+
runId: string;
|
|
39
|
+
turnId: string;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What the parent hands the child, and what the child then holds.
|
|
45
|
+
*
|
|
46
|
+
* It is the child's own durable state and the only thing in the child that
|
|
47
|
+
* outlives its Turn. Child-local by design (plan decision 2): journaling every
|
|
48
|
+
* child event back into the parent would double the write cost and make the
|
|
49
|
+
* parent a bottleneck for no added guarantee, and the parent already holds the
|
|
50
|
+
* record of admission and terminal state that recovery reads.
|
|
51
|
+
*/
|
|
52
|
+
export interface SubagentTaskContextV1 {
|
|
53
|
+
schemaVersion: 1;
|
|
54
|
+
taskId: string;
|
|
55
|
+
type: TaskTypeV1;
|
|
56
|
+
parent: SubagentParentV1;
|
|
57
|
+
compositionGenerationId: string;
|
|
58
|
+
model: TaskModelV1;
|
|
59
|
+
prompt: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
status: "queued" | "running" | "settled";
|
|
62
|
+
acceptedAt: string;
|
|
63
|
+
outcome?: TaskOutcomeV1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
68
|
+
throw new SubagentDecodeError(`${label} must be an object`);
|
|
69
|
+
}
|
|
70
|
+
return value as Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function decodeSubagentParentV1(
|
|
74
|
+
value: unknown,
|
|
75
|
+
label = "subagent parent",
|
|
76
|
+
): SubagentParentV1 {
|
|
77
|
+
const candidate = record(value, label);
|
|
78
|
+
subagentExactKeys(
|
|
79
|
+
candidate,
|
|
80
|
+
["userId", "botId", "runId", "turnId", "sessionId"],
|
|
81
|
+
[],
|
|
82
|
+
label,
|
|
83
|
+
);
|
|
84
|
+
return {
|
|
85
|
+
userId: subagentText(candidate.userId, 128, `${label}.userId`),
|
|
86
|
+
botId: subagentText(candidate.botId, 128, `${label}.botId`),
|
|
87
|
+
runId: subagentText(candidate.runId, 128, `${label}.runId`),
|
|
88
|
+
turnId: subagentText(candidate.turnId, 256, `${label}.turnId`),
|
|
89
|
+
sessionId: subagentText(candidate.sessionId, 256, `${label}.sessionId`),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The exact `runTask` payload, decoded at the Subagent object's door. */
|
|
94
|
+
export interface SubagentRunTaskRequestV1 {
|
|
95
|
+
taskId: string;
|
|
96
|
+
type: TaskTypeV1;
|
|
97
|
+
parent: SubagentParentV1;
|
|
98
|
+
compositionGenerationId: string;
|
|
99
|
+
model: TaskModelV1;
|
|
100
|
+
prompt: string;
|
|
101
|
+
/**
|
|
102
|
+
* The Session the child Turn runs on. Absent on a first dispatch, where it
|
|
103
|
+
* is the task's own; present on a resume, where it is the *resumed* task's,
|
|
104
|
+
* because the whole point of resuming is that the child picks its own
|
|
105
|
+
* transcript up from its own cursor rather than starting blank again.
|
|
106
|
+
*/
|
|
107
|
+
sessionId?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function decodeSubagentRunTaskRequestV1(
|
|
111
|
+
value: unknown,
|
|
112
|
+
): SubagentRunTaskRequestV1 {
|
|
113
|
+
const label = "runTask request";
|
|
114
|
+
const candidate = record(value, label);
|
|
115
|
+
subagentExactKeys(
|
|
116
|
+
candidate,
|
|
117
|
+
["taskId", "type", "parent", "compositionGenerationId", "model", "prompt"],
|
|
118
|
+
["sessionId"],
|
|
119
|
+
label,
|
|
120
|
+
);
|
|
121
|
+
if (!isTaskIdV1(candidate.taskId)) {
|
|
122
|
+
throw new SubagentDecodeError(`${label}.taskId is invalid`);
|
|
123
|
+
}
|
|
124
|
+
const type = TASK_TYPES_V1.find((known) => known === candidate.type);
|
|
125
|
+
if (!type) throw new SubagentDecodeError(`${label}.type is invalid`);
|
|
126
|
+
const prompt = subagentText(
|
|
127
|
+
candidate.prompt,
|
|
128
|
+
TASK_PROMPT_MAX_BYTES_V1,
|
|
129
|
+
`${label}.prompt`,
|
|
130
|
+
);
|
|
131
|
+
if (utf8ByteLengthV1(prompt) > TASK_PROMPT_MAX_BYTES_V1) {
|
|
132
|
+
throw new SubagentDecodeError(`${label}.prompt is too large`);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
taskId: candidate.taskId,
|
|
136
|
+
type,
|
|
137
|
+
parent: decodeSubagentParentV1(candidate.parent, `${label}.parent`),
|
|
138
|
+
compositionGenerationId: subagentText(
|
|
139
|
+
candidate.compositionGenerationId,
|
|
140
|
+
256,
|
|
141
|
+
`${label}.compositionGenerationId`,
|
|
142
|
+
),
|
|
143
|
+
model: decodeTaskModelV1(candidate.model, `${label}.model`),
|
|
144
|
+
prompt,
|
|
145
|
+
...(candidate.sessionId === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: {
|
|
148
|
+
sessionId: subagentText(
|
|
149
|
+
candidate.sessionId,
|
|
150
|
+
256,
|
|
151
|
+
`${label}.sessionId`,
|
|
152
|
+
),
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function subagentTaskContextV1(
|
|
158
|
+
request: SubagentRunTaskRequestV1,
|
|
159
|
+
acceptedAt: string,
|
|
160
|
+
): SubagentTaskContextV1 {
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
taskId: request.taskId,
|
|
164
|
+
type: request.type,
|
|
165
|
+
parent: request.parent,
|
|
166
|
+
compositionGenerationId: request.compositionGenerationId,
|
|
167
|
+
model: request.model,
|
|
168
|
+
prompt: request.prompt,
|
|
169
|
+
sessionId: request.sessionId ?? taskSessionIdV1(request.taskId),
|
|
170
|
+
status: "queued",
|
|
171
|
+
acceptedAt,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function decodeSubagentTaskContextV1(
|
|
176
|
+
value: unknown,
|
|
177
|
+
): SubagentTaskContextV1 {
|
|
178
|
+
const label = "subagent task context";
|
|
179
|
+
const candidate = record(value, label);
|
|
180
|
+
subagentExactKeys(
|
|
181
|
+
candidate,
|
|
182
|
+
[
|
|
183
|
+
"schemaVersion",
|
|
184
|
+
"taskId",
|
|
185
|
+
"type",
|
|
186
|
+
"parent",
|
|
187
|
+
"compositionGenerationId",
|
|
188
|
+
"model",
|
|
189
|
+
"prompt",
|
|
190
|
+
"sessionId",
|
|
191
|
+
"status",
|
|
192
|
+
"acceptedAt",
|
|
193
|
+
],
|
|
194
|
+
["outcome"],
|
|
195
|
+
label,
|
|
196
|
+
);
|
|
197
|
+
if (candidate.schemaVersion !== 1) {
|
|
198
|
+
throw new SubagentDecodeError(`${label} schemaVersion is unsupported`);
|
|
199
|
+
}
|
|
200
|
+
if (!isTaskIdV1(candidate.taskId)) {
|
|
201
|
+
throw new SubagentDecodeError(`${label}.taskId is invalid`);
|
|
202
|
+
}
|
|
203
|
+
const type = TASK_TYPES_V1.find((known) => known === candidate.type);
|
|
204
|
+
if (!type) throw new SubagentDecodeError(`${label}.type is invalid`);
|
|
205
|
+
if (
|
|
206
|
+
candidate.status !== "queued" &&
|
|
207
|
+
candidate.status !== "running" &&
|
|
208
|
+
candidate.status !== "settled"
|
|
209
|
+
) {
|
|
210
|
+
throw new SubagentDecodeError(`${label}.status is invalid`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
schemaVersion: 1,
|
|
214
|
+
taskId: candidate.taskId,
|
|
215
|
+
type,
|
|
216
|
+
parent: decodeSubagentParentV1(candidate.parent, `${label}.parent`),
|
|
217
|
+
compositionGenerationId: subagentText(
|
|
218
|
+
candidate.compositionGenerationId,
|
|
219
|
+
256,
|
|
220
|
+
`${label}.compositionGenerationId`,
|
|
221
|
+
),
|
|
222
|
+
model: decodeTaskModelV1(candidate.model, `${label}.model`),
|
|
223
|
+
prompt: subagentText(
|
|
224
|
+
candidate.prompt,
|
|
225
|
+
TASK_PROMPT_MAX_BYTES_V1,
|
|
226
|
+
`${label}.prompt`,
|
|
227
|
+
),
|
|
228
|
+
sessionId: subagentText(candidate.sessionId, 256, `${label}.sessionId`),
|
|
229
|
+
status: candidate.status,
|
|
230
|
+
acceptedAt: subagentTimestamp(candidate.acceptedAt, `${label}.acceptedAt`),
|
|
231
|
+
...(candidate.outcome === undefined
|
|
232
|
+
? {}
|
|
233
|
+
: {
|
|
234
|
+
outcome: decodeTaskOutcomeV1(candidate.outcome, `${label}.outcome`),
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The Durable Object addressing this Package needs and deliberately does not
|
|
241
|
+
* hold: the Subagent object for one task, and the parent object of a child.
|
|
242
|
+
* The Bot Durable Object supplies it, exactly as it supplies the authority.
|
|
243
|
+
*/
|
|
244
|
+
export interface SubagentDurableBindingV1 {
|
|
245
|
+
/**
|
|
246
|
+
* Hands one task to its Subagent Durable Object. The call returns as soon as
|
|
247
|
+
* the child has *recorded* the task and armed its own alarm — never after the
|
|
248
|
+
* Turn has run, so a dispatch never blocks the Turn that made it and no
|
|
249
|
+
* promise is left floating in an object that may be evicted.
|
|
250
|
+
*/
|
|
251
|
+
accept(
|
|
252
|
+
identity: BotIdentity,
|
|
253
|
+
anchorTaskId: string,
|
|
254
|
+
request: SubagentRunTaskRequestV1,
|
|
255
|
+
): Promise<{ childSessionId: string }>;
|
|
256
|
+
/** Asks a child what became of a task, for deadline reconciliation. */
|
|
257
|
+
probe(
|
|
258
|
+
identity: BotIdentity,
|
|
259
|
+
anchorTaskId: string,
|
|
260
|
+
taskId: string,
|
|
261
|
+
): Promise<SubagentTaskContextV1 | undefined>;
|
|
262
|
+
/**
|
|
263
|
+
* Explicit, authenticated cancellation, carried to the execution host.
|
|
264
|
+
*
|
|
265
|
+
* The parent has already recorded the intent when this is called, so a child
|
|
266
|
+
* that cannot be reached does not keep the task alive: the parent settles it
|
|
267
|
+
* `stopped` either way, and the child reads its own cancelled context back.
|
|
268
|
+
*/
|
|
269
|
+
stop(
|
|
270
|
+
identity: BotIdentity,
|
|
271
|
+
anchorTaskId: string,
|
|
272
|
+
taskId: string,
|
|
273
|
+
): Promise<void>;
|
|
274
|
+
/** Records one terminal outcome on the parent, from the child. */
|
|
275
|
+
settleOnParent(
|
|
276
|
+
parent: SubagentParentV1,
|
|
277
|
+
taskId: string,
|
|
278
|
+
outcome: TaskOutcomeV1,
|
|
279
|
+
): Promise<void>;
|
|
280
|
+
/**
|
|
281
|
+
* Claims the messages the parent has queued for one task, on the child's
|
|
282
|
+
* behalf, and marks them delivered in the parent's own transaction.
|
|
283
|
+
*
|
|
284
|
+
* The queue lives in the parent because the parent is the authority; the
|
|
285
|
+
* reader is the child, because the child is the one running. This is the one
|
|
286
|
+
* call that crosses in that direction *during* a Turn, and it is bounded by
|
|
287
|
+
* the queue's own bound (16).
|
|
288
|
+
*/
|
|
289
|
+
claimMessagesOnParent(
|
|
290
|
+
parent: SubagentParentV1,
|
|
291
|
+
taskId: string,
|
|
292
|
+
): Promise<readonly { seq: number; message: string }[]>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The exact shape a claim answers with, decoded at the child's door: the
|
|
297
|
+
* parent is trusted, the wire is not, and a message reaches a model.
|
|
298
|
+
*/
|
|
299
|
+
export function decodeClaimedTaskMessagesV1(
|
|
300
|
+
value: unknown,
|
|
301
|
+
): { seq: number; message: string }[] {
|
|
302
|
+
const answer = value as { messages?: unknown } | null | undefined;
|
|
303
|
+
if (!answer || !Array.isArray(answer.messages)) return [];
|
|
304
|
+
return answer.messages.slice(0, CLAIMED_TASK_MESSAGE_LIMIT).map((entry) => {
|
|
305
|
+
const candidate = record(entry, "claimed task message");
|
|
306
|
+
if (!Number.isSafeInteger(candidate.seq) || (candidate.seq as number) < 0) {
|
|
307
|
+
throw new SubagentDecodeError("claimed task message seq is invalid");
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
seq: candidate.seq as number,
|
|
311
|
+
message: subagentText(
|
|
312
|
+
candidate.message,
|
|
313
|
+
TASK_MESSAGE_MAX_V1,
|
|
314
|
+
"claimed task message",
|
|
315
|
+
),
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The queue's own bound, restated where the wire is decoded. */
|
|
321
|
+
const CLAIMED_TASK_MESSAGE_LIMIT = 16;
|
|
322
|
+
|
|
323
|
+
const TASK_ID_CHARACTER = /[^a-zA-Z0-9._-]/g;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The task id one tool call mints.
|
|
327
|
+
*
|
|
328
|
+
* Derived from the Turn's effect identifier, so a reconciled or retried call
|
|
329
|
+
* finds the task it already dispatched instead of dispatching a second child.
|
|
330
|
+
*/
|
|
331
|
+
export function subagentTaskIdV1(effectId: string): string {
|
|
332
|
+
const sanitized = effectId.replace(TASK_ID_CHARACTER, "-").slice(0, 120);
|
|
333
|
+
return `tk-${sanitized || "call"}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The outcome a settled child run hands its parent. */
|
|
337
|
+
export function subagentOutcomeForRunV1(
|
|
338
|
+
run: { status: string; responseText?: string; failure?: string } | undefined,
|
|
339
|
+
settledAt: string,
|
|
340
|
+
thrown?: unknown,
|
|
341
|
+
): TaskOutcomeV1 {
|
|
342
|
+
if (thrown !== undefined) {
|
|
343
|
+
return {
|
|
344
|
+
status: "failed",
|
|
345
|
+
settledAt,
|
|
346
|
+
failure: thrown instanceof Error ? thrown.message : String(thrown),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (!run) {
|
|
350
|
+
return {
|
|
351
|
+
status: "failed",
|
|
352
|
+
settledAt,
|
|
353
|
+
failure: "the subagent Turn recorded no run",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (run.status === "cancelled") {
|
|
357
|
+
return {
|
|
358
|
+
status: "stopped",
|
|
359
|
+
settledAt,
|
|
360
|
+
...(run.failure === undefined ? {} : { failure: run.failure }),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (run.status === "completed") {
|
|
364
|
+
const summary = run.responseText?.trim();
|
|
365
|
+
return {
|
|
366
|
+
status: "completed",
|
|
367
|
+
settledAt,
|
|
368
|
+
...(summary ? { summary } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
status: "failed",
|
|
373
|
+
settledAt,
|
|
374
|
+
failure: run.failure ?? `the subagent's run is ${run.status}`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The `BOT_STATES` namespace, as this Package's subagent binding.
|
|
380
|
+
*
|
|
381
|
+
* The Subagent Durable Object is the *same class* in the *same namespace* as
|
|
382
|
+
* the Bot's own object, named `<userId>:<botId>#task:<taskId>` (ADR 0017) —
|
|
383
|
+
* so there is no migration, no second identity, and no way for a caller to
|
|
384
|
+
* reach one: `#` is outside `PUBLIC_IDENTIFIER_PATTERN`, so no Bot id can
|
|
385
|
+
* contain it and the suffix can only ever be minted here.
|
|
386
|
+
*/
|
|
387
|
+
export function createBotSubagentDurableBindingV1(
|
|
388
|
+
namespace: DurableObjectNamespace,
|
|
389
|
+
): SubagentDurableBindingV1 {
|
|
390
|
+
const stub = (name: string) =>
|
|
391
|
+
// SAFETY: this namespace is bound to the Bot Durable Object class;
|
|
392
|
+
// generated Worker types do not expose its RPC surface.
|
|
393
|
+
namespace.get(namespace.idFromName(name)) as unknown as {
|
|
394
|
+
runTask(input: unknown): Promise<unknown>;
|
|
395
|
+
readSubagentTask(input: unknown): Promise<unknown>;
|
|
396
|
+
settleTask(input: unknown): Promise<unknown>;
|
|
397
|
+
stopSubagentTask(input: unknown): Promise<unknown>;
|
|
398
|
+
claimTaskMessages(input: unknown): Promise<unknown>;
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
accept: async (identity, anchorTaskId, request) => {
|
|
402
|
+
const answer = (await stub(
|
|
403
|
+
subagentDurableObjectNameV1({ ...identity, taskId: anchorTaskId }),
|
|
404
|
+
).runTask({
|
|
405
|
+
schemaVersion: 1,
|
|
406
|
+
userId: identity.userId,
|
|
407
|
+
botId: identity.botId,
|
|
408
|
+
request,
|
|
409
|
+
})) as { childSessionId?: unknown };
|
|
410
|
+
const childSessionId = answer?.childSessionId;
|
|
411
|
+
if (typeof childSessionId !== "string" || childSessionId.length === 0) {
|
|
412
|
+
throw new Error("the Subagent Durable Object accepted no session");
|
|
413
|
+
}
|
|
414
|
+
return { childSessionId };
|
|
415
|
+
},
|
|
416
|
+
probe: async (identity, anchorTaskId, taskId) => {
|
|
417
|
+
const answer = await stub(
|
|
418
|
+
subagentDurableObjectNameV1({ ...identity, taskId: anchorTaskId }),
|
|
419
|
+
).readSubagentTask({
|
|
420
|
+
schemaVersion: 1,
|
|
421
|
+
userId: identity.userId,
|
|
422
|
+
botId: identity.botId,
|
|
423
|
+
taskId,
|
|
424
|
+
});
|
|
425
|
+
if (answer === undefined || answer === null) return undefined;
|
|
426
|
+
return decodeSubagentTaskContextV1(JSON.parse(JSON.stringify(answer)));
|
|
427
|
+
},
|
|
428
|
+
stop: async (identity, anchorTaskId, taskId) => {
|
|
429
|
+
await stub(
|
|
430
|
+
subagentDurableObjectNameV1({ ...identity, taskId: anchorTaskId }),
|
|
431
|
+
).stopSubagentTask({
|
|
432
|
+
schemaVersion: 1,
|
|
433
|
+
userId: identity.userId,
|
|
434
|
+
botId: identity.botId,
|
|
435
|
+
taskId,
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
claimMessagesOnParent: async (parent, taskId) => {
|
|
439
|
+
const answer = (await stub(
|
|
440
|
+
`${parent.userId}:${parent.botId}`,
|
|
441
|
+
).claimTaskMessages({
|
|
442
|
+
schemaVersion: 1,
|
|
443
|
+
userId: parent.userId,
|
|
444
|
+
botId: parent.botId,
|
|
445
|
+
taskId,
|
|
446
|
+
})) as { messages?: unknown };
|
|
447
|
+
return decodeClaimedTaskMessagesV1(answer);
|
|
448
|
+
},
|
|
449
|
+
settleOnParent: async (parent, taskId, outcome) => {
|
|
450
|
+
await stub(`${parent.userId}:${parent.botId}`).settleTask({
|
|
451
|
+
schemaVersion: 1,
|
|
452
|
+
userId: parent.userId,
|
|
453
|
+
botId: parent.botId,
|
|
454
|
+
taskId,
|
|
455
|
+
outcome,
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|