@frockbot/plugin-subagents 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 +33 -0
- package/package.json +38 -6
- package/src/agent.test.ts +480 -0
- package/src/agent.ts +1078 -0
- package/src/backend.ts +193 -0
- package/src/index.ts +8 -0
- package/src/manifest.ts +3 -0
- package/src/models.test.ts +175 -0
- package/src/models.ts +185 -0
- package/src/quota.test.ts +82 -0
- package/src/quota.ts +222 -0
- package/src/records.test.ts +245 -0
- package/src/records.ts +649 -0
- package/src/roles.test.ts +60 -0
- package/src/roles.ts +76 -0
- package/src/shared.ts +232 -0
- package/src/storage-keys.ts +148 -0
- package/src/store.test.ts +500 -0
- package/src/store.ts +695 -0
- package/src/testing.ts +58 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/src/roles.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The five subagent roles, and the tool reach each one has.
|
|
2
|
+
//
|
|
3
|
+
// A role is the *second* ceiling dimension on the tool catalog. The first is
|
|
4
|
+
// the turn type: a `subagent` Turn is never offered `Task`, `user_voice`, or
|
|
5
|
+
// anything else a Package declared chat-only. The second is the role: within
|
|
6
|
+
// the work tools a subagent Turn does reach, `browserUse` reaches the browser
|
|
7
|
+
// and not the shell, and `watchVideo` reaches neither.
|
|
8
|
+
//
|
|
9
|
+
// The kernel holds none of this. `ToolRegistry` treats a role exactly as it
|
|
10
|
+
// treats a turn type — an opaque string a registration may narrow itself by —
|
|
11
|
+
// and every table below is *declaration*: a Package says which roles its tool
|
|
12
|
+
// serves, in its own tool definition or its own manifest Capability, and the
|
|
13
|
+
// registry intersects the two. This module is where the first-party Packages'
|
|
14
|
+
// answers are written down together so they can be read as one catalog and
|
|
15
|
+
// tested as one table.
|
|
16
|
+
//
|
|
17
|
+
// Reference: `docs/research/grokbot-computer.md` l.351–356.
|
|
18
|
+
|
|
19
|
+
import { TASK_TYPES_V1, type TaskTypeV1 } from "./records.js";
|
|
20
|
+
|
|
21
|
+
/** A subagent role. The same five names a `Task` may name as its `type`. */
|
|
22
|
+
export type SubagentRoleV1 = TaskTypeV1;
|
|
23
|
+
|
|
24
|
+
/** Every role, in catalog order. */
|
|
25
|
+
export const SUBAGENT_ROLES_V1: readonly SubagentRoleV1[] = TASK_TYPES_V1;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What each role is for, in one line, as the `Task` tool describes it and as
|
|
29
|
+
* the task list shows it.
|
|
30
|
+
*/
|
|
31
|
+
export const SUBAGENT_ROLE_SUMMARIES_V1: Record<SubagentRoleV1, string> = {
|
|
32
|
+
executor: "does general work with the Bot's full work toolset",
|
|
33
|
+
browserUse: "drives web pages through the browser, and nothing else",
|
|
34
|
+
computerUse: "drives the shared desktop: shell, screen, and browser",
|
|
35
|
+
watchVideo: "watches the attachments it was given and reports on them",
|
|
36
|
+
videoReview: "reviews the attachments it was given and reports on them",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The reach one tool declares, as the set of roles it is offered to.
|
|
41
|
+
*
|
|
42
|
+
* A Package writes the literal array in its own tool definition — these
|
|
43
|
+
* constants are the record of what the first-party answers *are*, not a
|
|
44
|
+
* runtime dependency: `plugin-computer` cannot import this Package, and should
|
|
45
|
+
* not have to, for the kernel to enforce the ceiling.
|
|
46
|
+
*/
|
|
47
|
+
export const SUBAGENT_TOOL_REACH_V1 = {
|
|
48
|
+
/**
|
|
49
|
+
* Reading, and the attachments a task was handed. Every role, including the
|
|
50
|
+
* two video roles, whose whole job is to read what they were given.
|
|
51
|
+
*/
|
|
52
|
+
read: SUBAGENT_ROLES_V1,
|
|
53
|
+
/** Handing the Turn back to the parent. Every role can finish. */
|
|
54
|
+
handoff: SUBAGENT_ROLES_V1,
|
|
55
|
+
/** General work tools: memory, skills, routines, MCP, the web, authoring. */
|
|
56
|
+
work: ["executor"],
|
|
57
|
+
/** Page-level browser control. */
|
|
58
|
+
browser: ["executor", "browserUse", "computerUse"],
|
|
59
|
+
/** The shell and the desktop screen: `computer_exec`, screenshots, processes. */
|
|
60
|
+
desktop: ["executor", "computerUse"],
|
|
61
|
+
} as const satisfies Record<string, readonly SubagentRoleV1[]>;
|
|
62
|
+
|
|
63
|
+
/** The reach names, for a table-driven test to walk. */
|
|
64
|
+
export type SubagentToolReachV1 = keyof typeof SUBAGENT_TOOL_REACH_V1;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a tool of this reach is offered to this role — the same predicate
|
|
68
|
+
* `ToolRegistry` applies, restated over the reach names so the catalog can be
|
|
69
|
+
* asserted as a table rather than as a mounted runtime.
|
|
70
|
+
*/
|
|
71
|
+
export function subagentRoleAdmitsV1(
|
|
72
|
+
role: SubagentRoleV1,
|
|
73
|
+
reach: SubagentToolReachV1,
|
|
74
|
+
): boolean {
|
|
75
|
+
return (SUBAGENT_TOOL_REACH_V1[reach] as readonly string[]).includes(role);
|
|
76
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The DTOs the task surface crosses seams with, and their exact codecs.
|
|
2
|
+
//
|
|
3
|
+
// A view is projected from a `TaskRecordV1` and carries strictly less: no
|
|
4
|
+
// prompt, no binding secrets, no child transcript. A child Session never enters
|
|
5
|
+
// the visible transcript at all (ADR 0017 — the child is an execution host, and
|
|
6
|
+
// its Session is its own durable state), so this list is the only door onto a
|
|
7
|
+
// task and it is deliberately a narrow one.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isTaskIdV1,
|
|
11
|
+
subagentExactKeys,
|
|
12
|
+
subagentText,
|
|
13
|
+
subagentTimestamp,
|
|
14
|
+
SubagentDecodeError,
|
|
15
|
+
TASK_DESCRIPTION_MAX_V1,
|
|
16
|
+
TASK_STATUSES_V1,
|
|
17
|
+
TASK_SUMMARY_MAX_V1,
|
|
18
|
+
TASK_TYPES_V1,
|
|
19
|
+
type TaskRecordV1,
|
|
20
|
+
type TaskStatusV1,
|
|
21
|
+
type TaskTypeV1,
|
|
22
|
+
} from "./records.js";
|
|
23
|
+
|
|
24
|
+
/** Most rows one task-list answer carries. */
|
|
25
|
+
export const TASK_LIST_LIMIT_V1 = 50;
|
|
26
|
+
|
|
27
|
+
export interface TaskViewV1 {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
taskId: string;
|
|
30
|
+
type: TaskTypeV1;
|
|
31
|
+
description: string;
|
|
32
|
+
status: TaskStatusV1;
|
|
33
|
+
model: string;
|
|
34
|
+
background: boolean;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
deadlineAt: string;
|
|
37
|
+
settledAt?: string;
|
|
38
|
+
summary?: string;
|
|
39
|
+
failure?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TaskListViewV1 {
|
|
43
|
+
schemaVersion: 1;
|
|
44
|
+
botId: string;
|
|
45
|
+
active: number;
|
|
46
|
+
tasks: TaskViewV1[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The view one durable record projects onto. Never the reverse. */
|
|
50
|
+
export function taskViewV1(record: TaskRecordV1): TaskViewV1 {
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
taskId: record.taskId,
|
|
54
|
+
type: record.type,
|
|
55
|
+
description: record.description,
|
|
56
|
+
status: record.status,
|
|
57
|
+
model: record.model.slug,
|
|
58
|
+
background: record.background,
|
|
59
|
+
createdAt: record.createdAt,
|
|
60
|
+
deadlineAt: record.deadlineAt,
|
|
61
|
+
...(record.outcome === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: {
|
|
64
|
+
settledAt: record.outcome.settledAt,
|
|
65
|
+
...(record.outcome.summary === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: { summary: record.outcome.summary }),
|
|
68
|
+
...(record.outcome.failure === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { failure: record.outcome.failure }),
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
76
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
77
|
+
throw new SubagentDecodeError(`${label} must be an object`);
|
|
78
|
+
}
|
|
79
|
+
return value as Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function decodeTaskViewV1(
|
|
83
|
+
value: unknown,
|
|
84
|
+
label = "task view",
|
|
85
|
+
): TaskViewV1 {
|
|
86
|
+
const candidate = record(value, label);
|
|
87
|
+
subagentExactKeys(
|
|
88
|
+
candidate,
|
|
89
|
+
[
|
|
90
|
+
"schemaVersion",
|
|
91
|
+
"taskId",
|
|
92
|
+
"type",
|
|
93
|
+
"description",
|
|
94
|
+
"status",
|
|
95
|
+
"model",
|
|
96
|
+
"background",
|
|
97
|
+
"createdAt",
|
|
98
|
+
"deadlineAt",
|
|
99
|
+
],
|
|
100
|
+
["settledAt", "summary", "failure"],
|
|
101
|
+
label,
|
|
102
|
+
);
|
|
103
|
+
if (candidate.schemaVersion !== 1) {
|
|
104
|
+
throw new SubagentDecodeError(`${label} schemaVersion is unsupported`);
|
|
105
|
+
}
|
|
106
|
+
if (!isTaskIdV1(candidate.taskId)) {
|
|
107
|
+
throw new SubagentDecodeError(`${label} taskId is invalid`);
|
|
108
|
+
}
|
|
109
|
+
const type = TASK_TYPES_V1.find((known) => known === candidate.type);
|
|
110
|
+
if (!type) throw new SubagentDecodeError(`${label} type is invalid`);
|
|
111
|
+
const status = TASK_STATUSES_V1.find((known) => known === candidate.status);
|
|
112
|
+
if (!status) throw new SubagentDecodeError(`${label} status is invalid`);
|
|
113
|
+
if (typeof candidate.background !== "boolean") {
|
|
114
|
+
throw new SubagentDecodeError(`${label} background must be a boolean`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
taskId: candidate.taskId,
|
|
119
|
+
type,
|
|
120
|
+
description: subagentText(
|
|
121
|
+
candidate.description,
|
|
122
|
+
TASK_DESCRIPTION_MAX_V1,
|
|
123
|
+
`${label}.description`,
|
|
124
|
+
),
|
|
125
|
+
status,
|
|
126
|
+
model: subagentText(candidate.model, 512, `${label}.model`),
|
|
127
|
+
background: candidate.background,
|
|
128
|
+
createdAt: subagentTimestamp(candidate.createdAt, `${label}.createdAt`),
|
|
129
|
+
deadlineAt: subagentTimestamp(candidate.deadlineAt, `${label}.deadlineAt`),
|
|
130
|
+
...(candidate.settledAt === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: {
|
|
133
|
+
settledAt: subagentTimestamp(
|
|
134
|
+
candidate.settledAt,
|
|
135
|
+
`${label}.settledAt`,
|
|
136
|
+
),
|
|
137
|
+
}),
|
|
138
|
+
...(candidate.summary === undefined
|
|
139
|
+
? {}
|
|
140
|
+
: {
|
|
141
|
+
summary: subagentText(
|
|
142
|
+
candidate.summary,
|
|
143
|
+
TASK_SUMMARY_MAX_V1,
|
|
144
|
+
`${label}.summary`,
|
|
145
|
+
),
|
|
146
|
+
}),
|
|
147
|
+
...(candidate.failure === undefined
|
|
148
|
+
? {}
|
|
149
|
+
: {
|
|
150
|
+
failure: subagentText(
|
|
151
|
+
candidate.failure,
|
|
152
|
+
TASK_SUMMARY_MAX_V1,
|
|
153
|
+
`${label}.failure`,
|
|
154
|
+
),
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function decodeTaskListViewV1(value: unknown): TaskListViewV1 {
|
|
160
|
+
const label = "task list";
|
|
161
|
+
const candidate = record(value, label);
|
|
162
|
+
subagentExactKeys(
|
|
163
|
+
candidate,
|
|
164
|
+
["schemaVersion", "botId", "active", "tasks"],
|
|
165
|
+
[],
|
|
166
|
+
label,
|
|
167
|
+
);
|
|
168
|
+
if (candidate.schemaVersion !== 1) {
|
|
169
|
+
throw new SubagentDecodeError(`${label} schemaVersion is unsupported`);
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
!Number.isSafeInteger(candidate.active) ||
|
|
173
|
+
(candidate.active as number) < 0
|
|
174
|
+
) {
|
|
175
|
+
throw new SubagentDecodeError(`${label} active is invalid`);
|
|
176
|
+
}
|
|
177
|
+
if (!Array.isArray(candidate.tasks)) {
|
|
178
|
+
throw new SubagentDecodeError(`${label} tasks must be an array`);
|
|
179
|
+
}
|
|
180
|
+
if (candidate.tasks.length > TASK_LIST_LIMIT_V1) {
|
|
181
|
+
throw new SubagentDecodeError(`${label} carries too many tasks`);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
botId: subagentText(candidate.botId, 128, `${label}.botId`),
|
|
186
|
+
active: candidate.active as number,
|
|
187
|
+
tasks: candidate.tasks.map((entry, index) =>
|
|
188
|
+
decodeTaskViewV1(entry, `${label}.tasks[${index}]`),
|
|
189
|
+
),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The dispatch one `Task` call becomes, once the tool input has been decoded. */
|
|
194
|
+
export interface TaskDispatchReceiptV1 {
|
|
195
|
+
schemaVersion: 1;
|
|
196
|
+
status: "dispatched";
|
|
197
|
+
taskId: string;
|
|
198
|
+
model: string;
|
|
199
|
+
background: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function decodeTaskDispatchReceiptV1(
|
|
203
|
+
value: unknown,
|
|
204
|
+
): TaskDispatchReceiptV1 {
|
|
205
|
+
const label = "task dispatch receipt";
|
|
206
|
+
const candidate = record(value, label);
|
|
207
|
+
subagentExactKeys(
|
|
208
|
+
candidate,
|
|
209
|
+
["schemaVersion", "status", "taskId", "model", "background"],
|
|
210
|
+
[],
|
|
211
|
+
label,
|
|
212
|
+
);
|
|
213
|
+
if (candidate.schemaVersion !== 1) {
|
|
214
|
+
throw new SubagentDecodeError(`${label} schemaVersion is unsupported`);
|
|
215
|
+
}
|
|
216
|
+
if (candidate.status !== "dispatched") {
|
|
217
|
+
throw new SubagentDecodeError(`${label} status is invalid`);
|
|
218
|
+
}
|
|
219
|
+
if (!isTaskIdV1(candidate.taskId)) {
|
|
220
|
+
throw new SubagentDecodeError(`${label} taskId is invalid`);
|
|
221
|
+
}
|
|
222
|
+
if (typeof candidate.background !== "boolean") {
|
|
223
|
+
throw new SubagentDecodeError(`${label} background must be a boolean`);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
schemaVersion: 1,
|
|
227
|
+
status: "dispatched",
|
|
228
|
+
taskId: candidate.taskId,
|
|
229
|
+
model: subagentText(candidate.model, 512, `${label}.model`),
|
|
230
|
+
background: candidate.background,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The Bot Durable Object storage keys the Subagents Package owns.
|
|
2
|
+
//
|
|
3
|
+
// They live here rather than in `@frockbot/kernel-do` because the kernel
|
|
4
|
+
// imports no Package and holds no product policy; the Durable Object hands this
|
|
5
|
+
// Package a storage seam and this module decides what it writes under.
|
|
6
|
+
//
|
|
7
|
+
// Every key below is *parent* state. ADR 0017: the parent Bot Durable Object is
|
|
8
|
+
// the authority for a task — admission, bounds, leases, lifecycle, terminal
|
|
9
|
+
// outcome — and the Subagent Durable Object holds only its own Session.
|
|
10
|
+
|
|
11
|
+
import { TASK_ID_MAX_V1 } from "./records.js";
|
|
12
|
+
|
|
13
|
+
/** One `TaskRecordV1`. */
|
|
14
|
+
export const TASK_PREFIX = "task:";
|
|
15
|
+
/**
|
|
16
|
+
* The membership set that *is* the per-Bot concurrency counter. Written in the
|
|
17
|
+
* same transaction that writes the record, deleted in the one that settles it,
|
|
18
|
+
* so counting keys and reading records can never disagree.
|
|
19
|
+
*/
|
|
20
|
+
export const TASK_ACTIVE_PREFIX = "task-active:";
|
|
21
|
+
/** One bounded queue of pending `task_message` payloads (G2 drains them). */
|
|
22
|
+
export const TASK_MESSAGE_PREFIX = "task-msg:";
|
|
23
|
+
/**
|
|
24
|
+
* The durable intent to cancel one task: written before the child is asked to
|
|
25
|
+
* stop, so an interrupted `task_stop` is read back rather than repeated, and
|
|
26
|
+
* deleted by the settle that makes the cancellation terminal.
|
|
27
|
+
*/
|
|
28
|
+
export const TASK_STOP_PREFIX = "task-stop:";
|
|
29
|
+
/** The `computerUse` desktop-lease intent: the effect named before the host call. */
|
|
30
|
+
export const TASK_DESKTOP_LEASE_KEY = "task-lease:desktop";
|
|
31
|
+
/** The bounded reverse index the Bot-level task list reads, newest first. */
|
|
32
|
+
export const TASK_INDEX_PREFIX = "task-index:";
|
|
33
|
+
/** The task index's monotonic sequence, addressable by key alone. */
|
|
34
|
+
export const TASK_INDEX_CURSOR_KEY = "task-index-cursor";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The child Durable Object's own record of what it was handed. It is the only
|
|
38
|
+
* key in this module written in the *child* object, and it is deliberately not
|
|
39
|
+
* authority: it records the parent, the pinned Composition generation, and the
|
|
40
|
+
* pinned binding so a child that is asked what it is doing can answer without
|
|
41
|
+
* the parent telling it again.
|
|
42
|
+
*/
|
|
43
|
+
export const TASK_CONTEXT_PREFIX = "task-context:";
|
|
44
|
+
|
|
45
|
+
/** Most index rows retained. Trimming loses an index row, never a task record. */
|
|
46
|
+
export const TASK_INDEX_LIMIT = 100;
|
|
47
|
+
|
|
48
|
+
const SEQUENCE_CEILING = 1_000_000_000;
|
|
49
|
+
|
|
50
|
+
function requireTaskId(taskId: string): string {
|
|
51
|
+
if (
|
|
52
|
+
typeof taskId !== "string" ||
|
|
53
|
+
taskId.length === 0 ||
|
|
54
|
+
taskId.length > TASK_ID_MAX_V1 ||
|
|
55
|
+
taskId.includes(":")
|
|
56
|
+
) {
|
|
57
|
+
throw new Error("task id is invalid");
|
|
58
|
+
}
|
|
59
|
+
return taskId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function taskKeyV1(taskId: string): string {
|
|
63
|
+
return `${TASK_PREFIX}${requireTaskId(taskId)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function taskActiveKeyV1(taskId: string): string {
|
|
67
|
+
return `${TASK_ACTIVE_PREFIX}${requireTaskId(taskId)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function taskContextKeyV1(taskId: string): string {
|
|
71
|
+
return `${TASK_CONTEXT_PREFIX}${requireTaskId(taskId)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function taskStopKeyV1(taskId: string): string {
|
|
75
|
+
return `${TASK_STOP_PREFIX}${requireTaskId(taskId)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function taskMessagePrefixV1(taskId: string): string {
|
|
79
|
+
return `${TASK_MESSAGE_PREFIX}${requireTaskId(taskId)}:`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Message keys ascend, so a prefix listing drains oldest first. */
|
|
83
|
+
export function taskMessageKeyV1(taskId: string, seq: number): string {
|
|
84
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || seq >= SEQUENCE_CEILING) {
|
|
85
|
+
throw new Error("task message sequence is out of range");
|
|
86
|
+
}
|
|
87
|
+
return `${taskMessagePrefixV1(taskId)}${String(seq).padStart(10, "0")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Index keys descend, so a prefix listing returns the newest task first without
|
|
92
|
+
* reading every record.
|
|
93
|
+
*/
|
|
94
|
+
export function taskIndexKeyV1(seq: number): string {
|
|
95
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || seq >= SEQUENCE_CEILING) {
|
|
96
|
+
throw new Error("task index sequence is out of range");
|
|
97
|
+
}
|
|
98
|
+
return `${TASK_INDEX_PREFIX}${String(SEQUENCE_CEILING - seq).padStart(10, "0")}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The sequence the next index row takes, given the keys already stored. */
|
|
102
|
+
export function nextTaskIndexSequenceV1(keys: readonly string[]): number {
|
|
103
|
+
let highest = -1;
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
const encoded = Number(key.slice(key.lastIndexOf(":") + 1));
|
|
106
|
+
if (!Number.isSafeInteger(encoded)) continue;
|
|
107
|
+
highest = Math.max(highest, SEQUENCE_CEILING - encoded);
|
|
108
|
+
}
|
|
109
|
+
return highest + 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The name of the Subagent Durable Object one task runs in.
|
|
114
|
+
*
|
|
115
|
+
* The same `BotState` class in the same `BOT_STATES` namespace, so there is no
|
|
116
|
+
* migration and no second identity: `#` cannot appear in a Bot id (it is
|
|
117
|
+
* outside `PUBLIC_IDENTIFIER_PATTERN`), so the suffix is unforgeable from any
|
|
118
|
+
* caller-supplied path segment and a Subagent object can never collide with a
|
|
119
|
+
* Bot object.
|
|
120
|
+
*/
|
|
121
|
+
export function subagentDurableObjectNameV1(identity: {
|
|
122
|
+
userId: string;
|
|
123
|
+
botId: string;
|
|
124
|
+
taskId: string;
|
|
125
|
+
}): string {
|
|
126
|
+
return `${identity.userId}:${identity.botId}#task:${requireTaskId(identity.taskId)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The Session a child Turn records its own events on. */
|
|
130
|
+
export function taskSessionIdV1(taskId: string): string {
|
|
131
|
+
return `task:${requireTaskId(taskId)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The task whose Session and Subagent Durable Object a child Turn actually
|
|
136
|
+
* runs in — the *anchor*.
|
|
137
|
+
*
|
|
138
|
+
* For a first dispatch that is the task itself. For a resume it is the task
|
|
139
|
+
* that was resumed, because "the same child Durable Object and Session" is the
|
|
140
|
+
* whole point of resuming: the child picks its prior transcript up from its own
|
|
141
|
+
* cursor rather than starting blank a second time.
|
|
142
|
+
*/
|
|
143
|
+
export function taskAnchorIdV1(childSessionId: string): string {
|
|
144
|
+
const anchor = childSessionId.startsWith("task:")
|
|
145
|
+
? childSessionId.slice("task:".length)
|
|
146
|
+
: childSessionId;
|
|
147
|
+
return requireTaskId(anchor);
|
|
148
|
+
}
|