@agentorg/cli 0.0.6 → 0.0.7

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.
Files changed (108) hide show
  1. package/README.md +10 -7
  2. package/dist/node_modules/@agentorg/computer/LICENSE +201 -0
  3. package/dist/node_modules/@agentorg/computer/NOTICE +2 -0
  4. package/dist/node_modules/@agentorg/computer/dist/THIRD_PARTY_LICENSES.txt +1321 -0
  5. package/dist/node_modules/@agentorg/computer/dist/sdk.js +24909 -0
  6. package/dist/node_modules/@agentorg/computer/dist/types/generated/schema.d.ts +860 -0
  7. package/dist/node_modules/@agentorg/computer/dist/types/sdk.d.ts +242 -0
  8. package/dist/node_modules/@agentorg/computer/package.json +12 -0
  9. package/dist/node_modules/@agentorg/computer-connector/dist/src/index.d.ts +1 -0
  10. package/dist/node_modules/@agentorg/computer-connector/dist/src/index.js +1 -0
  11. package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.d.ts +24 -0
  12. package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.js +482 -0
  13. package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.d.ts +19 -0
  14. package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.js +149 -0
  15. package/dist/node_modules/@agentorg/computer-connector/package.json +33 -0
  16. package/dist/node_modules/@agentorg/computer-connector/skills/browser/SKILL.md +31 -0
  17. package/dist/node_modules/@agentorg/computer-connector/skills/computer/SKILL.md +30 -0
  18. package/dist/node_modules/@agentorg/computer-connector/skills/files/SKILL.md +28 -0
  19. package/dist/node_modules/@agentorg/computer-connector/skills/terminal/SKILL.md +39 -0
  20. package/dist/node_modules/@agentorg/computer-connector/skills/web-research/SKILL.md +29 -0
  21. package/dist/node_modules/@agentorg/hub/dist/src/router.d.ts +4 -2
  22. package/dist/node_modules/@agentorg/hub/dist/src/router.js +3 -1
  23. package/dist/node_modules/@agentorg/hub/dist/src/validate.js +16 -2
  24. package/dist/node_modules/@agentorg/interfaces/dist/src/agent-provider.d.ts +34 -47
  25. package/dist/node_modules/@agentorg/interfaces/dist/src/agent-provider.js +25 -34
  26. package/dist/node_modules/@agentorg/interfaces/dist/src/chat.d.ts +90 -50
  27. package/dist/node_modules/@agentorg/interfaces/dist/src/chat.js +69 -34
  28. package/dist/node_modules/@agentorg/interfaces/dist/src/connection.d.ts +36 -0
  29. package/dist/node_modules/@agentorg/interfaces/dist/src/connection.js +31 -0
  30. package/dist/node_modules/@agentorg/interfaces/dist/src/folders.d.ts +19 -0
  31. package/dist/node_modules/@agentorg/interfaces/dist/src/folders.js +25 -0
  32. package/dist/node_modules/@agentorg/interfaces/dist/src/index.d.ts +6 -4
  33. package/dist/node_modules/@agentorg/interfaces/dist/src/index.js +6 -4
  34. package/dist/node_modules/@agentorg/interfaces/dist/src/llm-session.d.ts +2 -0
  35. package/dist/node_modules/@agentorg/interfaces/dist/src/llm-session.js +2 -1
  36. package/dist/node_modules/@agentorg/interfaces/dist/src/manager.d.ts +381 -27
  37. package/dist/node_modules/@agentorg/interfaces/dist/src/manager.js +179 -15
  38. package/dist/node_modules/@agentorg/interfaces/dist/src/units.d.ts +460 -0
  39. package/dist/node_modules/@agentorg/interfaces/dist/src/units.js +204 -0
  40. package/dist/node_modules/@agentorg/looper/dist/src/index.d.ts +5 -0
  41. package/dist/node_modules/@agentorg/looper/dist/src/index.js +5 -0
  42. package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +75 -0
  43. package/dist/node_modules/@agentorg/looper/dist/src/loop.js +332 -0
  44. package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +71 -0
  45. package/dist/node_modules/@agentorg/looper/dist/src/looper.js +282 -0
  46. package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +99 -0
  47. package/dist/node_modules/{assistant/dist/src/history.js → @agentorg/looper/dist/src/record.js} +89 -46
  48. package/dist/node_modules/@agentorg/looper/dist/src/tools.d.ts +11 -0
  49. package/dist/node_modules/@agentorg/looper/dist/src/tools.js +42 -0
  50. package/dist/node_modules/@agentorg/looper/dist/src/types.d.ts +147 -0
  51. package/dist/node_modules/@agentorg/looper/dist/src/types.js +32 -0
  52. package/dist/node_modules/@agentorg/looper/package.json +24 -0
  53. package/dist/node_modules/@agentorg/manager/dist/src/activity.d.ts +18 -0
  54. package/dist/node_modules/@agentorg/manager/dist/src/activity.js +65 -0
  55. package/dist/node_modules/@agentorg/manager/dist/src/control.d.ts +134 -0
  56. package/dist/node_modules/@agentorg/manager/dist/src/control.js +1115 -0
  57. package/dist/node_modules/@agentorg/manager/dist/src/form.d.ts +40 -0
  58. package/dist/node_modules/@agentorg/manager/dist/src/form.js +191 -0
  59. package/dist/node_modules/@agentorg/manager/dist/src/manager.d.ts +4 -0
  60. package/dist/node_modules/@agentorg/manager/dist/src/manager.js +341 -189
  61. package/dist/node_modules/@agentorg/manager/dist/src/skills.d.ts +2 -0
  62. package/dist/node_modules/@agentorg/manager/dist/src/skills.js +7 -0
  63. package/dist/node_modules/@agentorg/manager/dist/src/stack.d.ts +454 -0
  64. package/dist/node_modules/@agentorg/manager/dist/src/stack.js +917 -0
  65. package/dist/node_modules/@agentorg/mcp/dist/src/connection.d.ts +2 -1
  66. package/dist/node_modules/@agentorg/mcp/dist/src/connection.js +8 -2
  67. package/dist/node_modules/@agentorg/mcp/dist/src/index.d.ts +3 -0
  68. package/dist/node_modules/@agentorg/mcp/dist/src/index.js +3 -0
  69. package/dist/node_modules/@agentorg/mcp/dist/src/kinds.d.ts +108 -0
  70. package/dist/node_modules/@agentorg/mcp/dist/src/kinds.js +95 -0
  71. package/dist/node_modules/@agentorg/mcp/dist/src/provider.d.ts +2 -2
  72. package/dist/node_modules/@agentorg/mcp/dist/src/provider.js +48 -18
  73. package/dist/node_modules/@agentorg/mcp/dist/src/public.d.ts +6 -0
  74. package/dist/node_modules/@agentorg/mcp/dist/src/public.js +72 -0
  75. package/dist/node_modules/@agentorg/sdk/dist/src/filled.d.ts +21 -0
  76. package/dist/node_modules/@agentorg/sdk/dist/src/filled.js +24 -0
  77. package/dist/node_modules/@agentorg/sdk/dist/src/index.d.ts +1 -0
  78. package/dist/node_modules/@agentorg/sdk/dist/src/index.js +1 -0
  79. package/dist/node_modules/@agentorg/shell/dist/app.css +1289 -100
  80. package/dist/node_modules/@agentorg/shell/dist/app.js +207 -166
  81. package/dist/node_modules/@agentorg/shell/package.json +6 -1
  82. package/dist/node_modules/assistant/dist/src/chats.d.ts +92 -0
  83. package/dist/node_modules/assistant/dist/src/chats.js +357 -0
  84. package/dist/node_modules/assistant/dist/src/engine.d.ts +26 -7
  85. package/dist/node_modules/assistant/dist/src/engine.js +107 -35
  86. package/dist/node_modules/assistant/dist/src/local-store.js +1 -1
  87. package/dist/node_modules/assistant/dist/src/provider.d.ts +1 -1
  88. package/dist/node_modules/assistant/dist/src/provider.js +47 -19
  89. package/dist/node_modules/assistant/dist/src/tools.d.ts +11 -28
  90. package/dist/node_modules/assistant/dist/src/tools.js +19 -40
  91. package/dist/node_modules/assistant/package.json +1 -0
  92. package/dist/src/api.js +4 -0
  93. package/dist/src/folders.d.ts +2 -0
  94. package/dist/src/folders.js +45 -0
  95. package/dist/src/gateway.d.ts +7 -1
  96. package/dist/src/gateway.js +81 -12
  97. package/dist/src/live.d.ts +17 -0
  98. package/dist/src/live.js +109 -0
  99. package/dist/src/programs.d.ts +7 -0
  100. package/dist/src/programs.js +29 -2
  101. package/dist/src/signin.d.ts +12 -3
  102. package/dist/src/signin.js +30 -6
  103. package/package.json +2 -1
  104. package/dist/node_modules/@agentorg/interfaces/dist/src/mcp-provider.d.ts +0 -88
  105. package/dist/node_modules/@agentorg/interfaces/dist/src/mcp-provider.js +0 -78
  106. package/dist/node_modules/assistant/dist/src/agent.d.ts +0 -62
  107. package/dist/node_modules/assistant/dist/src/agent.js +0 -226
  108. package/dist/node_modules/assistant/dist/src/history.d.ts +0 -97
@@ -0,0 +1,917 @@
1
+ /**
2
+ * An agent built as a stack of units on a base: checking one, applying it all or none, keeping its
3
+ * instances, removing them last first, and the templates that prefill one.
4
+ */
5
+ import { ConnectionOrigin, FieldValues, ResultKind, SkillName, SlotName, Template, UnitResult, } from "@agentorg/interfaces";
6
+ import { Refused } from "@agentorg/sdk";
7
+ import { z } from "zod";
8
+ import { AGENTS_SLOT, AGENTS_UNIT, agentsUnit, reachOf, reachProblems, skillFile, } from "./control.js";
9
+ import { formProblems, settle, workedOut } from "./form.js";
10
+ /** A unit an agent has: what its provider made, and what was filled in. Never a secret. */
11
+ const UnitRecord = z.object({
12
+ /** Management's own id for it. */
13
+ instance: z.string(),
14
+ provider: z.string(),
15
+ /** The provider's id for it. */
16
+ providerInstance: z.string(),
17
+ unit: z.string(),
18
+ title: z.string(),
19
+ values: FieldValues,
20
+ /** By kind, the instance it took each result from. */
21
+ needs: z.record(ResultKind, z.string()),
22
+ results: z.array(UnitResult),
23
+ connections: z.array(z.object({ slot: z.string(), connection: z.string() })),
24
+ skills: z.array(z.string()),
25
+ /** It was added as the MCP unit, and reads back as one. */
26
+ viaMcp: z.literal(true).optional(),
27
+ });
28
+ /** What only management knows of an agent: where it came from, what it is told, and its skills. */
29
+ export const AgentRecord = z.object({
30
+ provider: z.string(),
31
+ creatable: z.string(),
32
+ instructions: z.string(),
33
+ skills: z.array(z.string()),
34
+ purpose: z.string().optional(),
35
+ template: z.string().optional(),
36
+ units: z.array(UnitRecord).default([]),
37
+ /** Its export that is its chats, as its provider named it; `chat` for one kept before this was. */
38
+ chat: z.string().default("chat"),
39
+ /** The agent that made it, which may be gone by now. */
40
+ parent: z.object({ agent: z.string(), name: z.string() }).optional(),
41
+ /** The parent's computer, when it shares one: its own reach, for agents it makes. */
42
+ computer: z
43
+ .object({
44
+ access: z.string(),
45
+ service: z.string().optional(),
46
+ browser: z.boolean(),
47
+ connections: z.array(z.object({ slot: z.string(), connection: z.string() })),
48
+ skills: z.array(z.string()),
49
+ })
50
+ .optional(),
51
+ });
52
+ export const Records = z.object({
53
+ agents: z.record(z.string(), AgentRecord),
54
+ connections: z.record(z.string(), ConnectionOrigin),
55
+ templates: z.record(z.string(), Template).default({}),
56
+ /** A connection the person made, to its provider's id for it; none means they are the same. */
57
+ instances: z.record(z.string(), z.string()).default({}),
58
+ /** The agent that added a skill or saved a template, which only it may replace or remove. */
59
+ authors: z
60
+ .object({
61
+ skills: z.record(z.string(), z.string()).default({}),
62
+ templates: z.record(z.string(), z.string()).default({}),
63
+ })
64
+ .default({ skills: {}, templates: {} }),
65
+ });
66
+ /** Management's own units. They ask no provider: each is an exposure and is read back as one. */
67
+ const SKILL_UNIT = "skill";
68
+ const CONNECTION_UNIT = "connection";
69
+ /** One exposure, however it's reached: it stands for the Connection unit or a kind of connection. */
70
+ const MCP_UNIT = "mcp";
71
+ const MCP_SLOT = "slot";
72
+ const MCP_HOW = "how";
73
+ const skillInstance = (name) => `skill:${name}`;
74
+ const slotInstance = (slot) => `slot:${slot}`;
75
+ const MAX_MESSAGE = 2000;
76
+ /** How many templates one agent may save. */
77
+ const AGENT_TEMPLATES = 32;
78
+ const MAX_OPTIONS = 256;
79
+ const MAX_PROBLEMS = 256;
80
+ const MAX_NAME = 128;
81
+ const TOOLS = "tools@1";
82
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
83
+ const alreadyGone = (error) => error instanceof Refused && error.code === "unknown";
84
+ const refusal = (problems) => new Refused("invalid", problems
85
+ .map((p) => p.message)
86
+ .join(" ")
87
+ .slice(0, MAX_MESSAGE));
88
+ /** A unit that makes one connection and nothing else, by the field that names it, is a kind. */
89
+ export const slotFieldOf = ({ needs, gives, form }) => {
90
+ const [slot] = gives.slots;
91
+ return slot &&
92
+ "field" in slot &&
93
+ gives.slots.length === 1 &&
94
+ !needs.length &&
95
+ !gives.skills.length &&
96
+ !gives.results.length &&
97
+ !form.some((field) => field.adds || field.options?.some((option) => option.adds))
98
+ ? slot.field
99
+ : undefined;
100
+ };
101
+ export function createStacks(parts) {
102
+ const { self, hub, records, save, library, creating, log, changed, recordOf } = parts;
103
+ const queues = new Map();
104
+ /** One change to an agent at a time, in the order they came: each spans many calls. */
105
+ const serial = (agent, work) => {
106
+ const running = (queues.get(agent) ?? Promise.resolve()).then(work);
107
+ const done = () => {
108
+ if (queues.get(agent) === settled)
109
+ queues.delete(agent);
110
+ };
111
+ const settled = running.then(done, done);
112
+ queues.set(agent, settled);
113
+ return running;
114
+ };
115
+ const isAgentsUnit = (offer) => offer.provider === self && offer.unit.id === AGENTS_UNIT;
116
+ /** The connections the person made, which an agents unit can list as places. */
117
+ const places = async () => {
118
+ const actors = await parts.openActors();
119
+ return Object.entries(records.connections).flatMap(([connection, origin]) => {
120
+ const actor = actors.get(connection);
121
+ return actor && !origin.madeFor ? [{ value: connection, title: actor.name }] : [];
122
+ });
123
+ };
124
+ /** The MCP unit: a connection there is, or any kind on offer, each with its own fields. */
125
+ const mcpUnit = (kinds) => {
126
+ const fields = new Map();
127
+ for (const kind of kinds)
128
+ for (const field of kind.form) {
129
+ if (field.id === slotFieldOf(kind) ||
130
+ [MCP_HOW, MCP_SLOT, CONNECTION_UNIT].includes(field.id))
131
+ continue;
132
+ const shared = fields.get(field.id);
133
+ if (shared?.visible) {
134
+ shared.visible.in.push(kind.id);
135
+ // Shared, it's open to an agent only if every kind says so.
136
+ if (shared.authority !== field.authority)
137
+ delete shared.authority;
138
+ }
139
+ else
140
+ fields.set(field.id, { ...field, visible: { field: MCP_HOW, in: [kind.id] } });
141
+ }
142
+ return {
143
+ id: MCP_UNIT,
144
+ title: "MCP",
145
+ description: "A server's tools, for this agent to use.",
146
+ form: [
147
+ {
148
+ id: MCP_HOW,
149
+ type: "choice",
150
+ value: "string",
151
+ title: "How",
152
+ required: true,
153
+ options: [
154
+ { value: CONNECTION_UNIT, title: "A connection you have" },
155
+ ...kinds.map(({ id, title, description }) => ({ value: id, title, description })),
156
+ ],
157
+ },
158
+ {
159
+ id: CONNECTION_UNIT,
160
+ type: "connection",
161
+ value: "string",
162
+ title: "Connection",
163
+ authority: "holds",
164
+ required: true,
165
+ visible: { field: MCP_HOW, in: [CONNECTION_UNIT] },
166
+ },
167
+ ...fields.values(),
168
+ { id: MCP_SLOT, type: "text", value: "string", title: "Name", required: true },
169
+ ],
170
+ needs: [],
171
+ gives: { slots: [{ field: MCP_SLOT }], skills: [], results: [] },
172
+ };
173
+ };
174
+ const ownUnits = async (kinds) => [
175
+ mcpUnit(kinds),
176
+ agentsUnit(templates()
177
+ .slice(0, MAX_OPTIONS)
178
+ .map(({ id, title, description }) => ({ value: id, title, description })), (await places()).slice(0, MAX_OPTIONS - 1)),
179
+ {
180
+ id: SKILL_UNIT,
181
+ title: "Skill",
182
+ description: "A skill from the library.",
183
+ form: [
184
+ {
185
+ id: "skill",
186
+ authority: "holds",
187
+ type: "choice",
188
+ value: "string",
189
+ title: "Skill",
190
+ required: true,
191
+ options: library
192
+ .list()
193
+ .slice(0, MAX_OPTIONS)
194
+ .map(({ name, description }) => ({ value: name, title: name, description })),
195
+ },
196
+ ],
197
+ needs: [],
198
+ gives: { slots: [], skills: [{ field: "skill" }], results: [] },
199
+ },
200
+ {
201
+ id: CONNECTION_UNIT,
202
+ title: "Connection",
203
+ description: "A connection that already exists, under a name the agent sees.",
204
+ form: [
205
+ {
206
+ id: "connection",
207
+ authority: "holds",
208
+ type: "connection",
209
+ value: "string",
210
+ title: "Connection",
211
+ required: true,
212
+ },
213
+ { id: "slot", type: "text", value: "string", title: "Name", required: true },
214
+ ],
215
+ needs: [],
216
+ gives: { slots: [{ field: "slot" }], skills: [], results: [] },
217
+ },
218
+ ];
219
+ /** Every unit on offer now; a provider that doesn't answer offers none. */
220
+ const catalog = async () => {
221
+ const [providerName, others] = await Promise.all([
222
+ parts.nameOf(self),
223
+ parts.fromEach(parts.unitProviders(), async ({ actor, call }) => {
224
+ const name = await parts.nameOf(actor);
225
+ return (await call.units({})).units.map((unit) => ({ unit, provider: actor, providerName: name, call }));
226
+ }),
227
+ ]);
228
+ const offers = others.flat();
229
+ const kinds = offers.map((offer) => offer.unit).filter((unit) => slotFieldOf(unit));
230
+ return [
231
+ ...(await ownUnits(kinds)).map((unit) => ({ unit, provider: self, providerName })),
232
+ ...offers,
233
+ ];
234
+ };
235
+ const baseOf = async (base) => {
236
+ const provider = parts.agentProviders().find((one) => one.actor === base.provider);
237
+ if (!provider)
238
+ throw new Refused("unknown", `${base.provider} isn't a connected agent provider.`);
239
+ const { creatables } = await provider.call.creatable({});
240
+ const creatable = creatables.find((one) => one.id === base.creatable);
241
+ if (!creatable)
242
+ throw new Refused("unknown", `${base.provider} offers no ${base.creatable}.`);
243
+ return { provider, creatable };
244
+ };
245
+ /** The unit's own logic on what was filled in; one that can't say is a problem. */
246
+ const unitProblems = async (offer, values, secrets) => {
247
+ if (!offer.call) {
248
+ if (offer.unit.id !== CONNECTION_UNIT)
249
+ return [];
250
+ return (await parts.openActors()).has(String(values.connection))
251
+ ? []
252
+ : [{ field: "connection", message: "That connection doesn't exist." }];
253
+ }
254
+ try {
255
+ return (await offer.call.check({ unit: offer.unit.id, values, secrets })).problems;
256
+ }
257
+ catch (error) {
258
+ return [
259
+ { message: `This unit couldn't check it: ${messageOf(error)}`.slice(0, MAX_MESSAGE) },
260
+ ];
261
+ }
262
+ };
263
+ /** What the MCP unit stands for, as it was filled in: the unit that does the work. */
264
+ const throughMcp = (asked, offers) => {
265
+ const own = offers.find((one) => one.provider === self && one.unit.id === MCP_UNIT);
266
+ if (asked.provider !== self || asked.unit !== MCP_UNIT || !own)
267
+ return { filled: asked };
268
+ const settled = settle(own.unit.form, asked.values ?? {}, Object.keys(asked.secrets ?? {}));
269
+ if (settled.problems.length)
270
+ return { problems: settled.problems };
271
+ const { [MCP_HOW]: how, [MCP_SLOT]: slot, ...values } = settled.values;
272
+ if (how === CONNECTION_UNIT)
273
+ return {
274
+ filled: {
275
+ provider: self,
276
+ unit: CONNECTION_UNIT,
277
+ values: { connection: values.connection, slot: slot },
278
+ },
279
+ };
280
+ const kind = offers.find((one) => one.provider !== self && one.unit.id === how && slotFieldOf(one.unit));
281
+ if (!kind)
282
+ return { problems: [{ field: MCP_HOW, message: "That way isn't on offer now." }] };
283
+ return {
284
+ viaMcp: true,
285
+ filled: {
286
+ provider: kind.provider,
287
+ unit: kind.unit.id,
288
+ values: { ...values, [slotFieldOf(kind.unit)]: slot },
289
+ secrets: Object.fromEntries(Object.entries(asked.secrets ?? {}).filter(([name]) => settled.secrets.includes(name))),
290
+ },
291
+ };
292
+ };
293
+ /** Everything that can be known about one unit before anything runs. `taken` grows by it. */
294
+ const place = async (asked, picks, taken, offers, ref) => {
295
+ const through = throughMcp(asked, offers);
296
+ if (!through.filled)
297
+ return { problems: through.problems };
298
+ const { filled } = through;
299
+ const offer = offers.find((one) => one.provider === filled.provider && one.unit.id === filled.unit);
300
+ if (!offer)
301
+ return { problems: [{ message: `${filled.provider} offers no unit ${filled.unit}.` }] };
302
+ const { unit } = offer;
303
+ const malformed = formProblems(unit.form);
304
+ if (malformed.length)
305
+ return { problems: malformed.map((message) => ({ message })) };
306
+ const given = filled.secrets ?? {};
307
+ const settled = settle(unit.form, filled.values ?? {}, Object.keys(given));
308
+ const problems = [...settled.problems];
309
+ const needs = {};
310
+ for (const kind of Object.keys(picks))
311
+ if (!unit.needs.includes(kind))
312
+ problems.push({ message: `${unit.title} doesn't need ${kind}.` });
313
+ for (const kind of unit.needs) {
314
+ const able = taken.givers.filter((giver) => giver.provider === offer.provider && giver.results.includes(kind));
315
+ const pick = picks[kind];
316
+ const giver = pick === undefined
317
+ ? able.length === 1
318
+ ? able[0]
319
+ : undefined
320
+ : able.find((one) => one.ref === pick);
321
+ if (giver)
322
+ needs[kind] = giver.ref;
323
+ else
324
+ problems.push({
325
+ message: pick !== undefined
326
+ ? `${unit.title} can't take ${kind} from the unit picked.`
327
+ : able.length
328
+ ? `${unit.title} needs one ${kind}: pick which.`
329
+ : `${unit.title} needs a ${kind} from a unit above it.`,
330
+ });
331
+ }
332
+ const worked = workedOut(unit, settled.values);
333
+ for (const slot of worked.slots) {
334
+ if (!SlotName.safeParse(slot).success)
335
+ problems.push({ message: `${slot} can't name a connection.` });
336
+ if (taken.slots.has(slot))
337
+ problems.push({ message: `The name ${slot} is already taken.` });
338
+ taken.slots.add(slot);
339
+ }
340
+ for (const skill of worked.skills) {
341
+ if (!SkillName.safeParse(skill.name).success)
342
+ problems.push({ message: `${skill.name} can't name a skill.` });
343
+ else if (!skill.files && !library.has(skill.name))
344
+ problems.push({ message: `There is no skill ${skill.name}.` });
345
+ if (taken.skills.has(skill.name))
346
+ problems.push({ message: `Two units give the skill ${skill.name}.` });
347
+ taken.skills.add(skill.name);
348
+ }
349
+ if (ref !== undefined)
350
+ taken.givers.push({ ref, provider: offer.provider, results: unit.gives.results });
351
+ if (settled.problems.length === 0)
352
+ problems.push(...(await unitProblems(offer, settled.values, settled.secrets)));
353
+ const secrets = Object.fromEntries(Object.entries(given).filter(([name]) => settled.secrets.includes(name)));
354
+ return {
355
+ problems: problems.map((one) => ({ ...one, message: one.message.slice(0, MAX_MESSAGE) })),
356
+ placed: {
357
+ offer,
358
+ values: settled.values,
359
+ secrets,
360
+ worked,
361
+ needs,
362
+ ...(through.viaMcp ? { viaMcp: true } : {}),
363
+ },
364
+ };
365
+ };
366
+ const plan = async (units, reach) => {
367
+ const offers = await catalog();
368
+ const taken = { slots: new Set(), skills: new Set(), givers: [] };
369
+ const placed = [];
370
+ const problems = [];
371
+ for (const [position, unit] of units.entries()) {
372
+ const one = await place(unit, unit.needs ?? {}, taken, offers, position);
373
+ problems.push(...one.problems.map((problem) => ({ ...problem, unit: position })));
374
+ if (one.placed)
375
+ placed.push(one.placed);
376
+ }
377
+ const computer = placed.find((one) => one.offer.unit.id === "computer");
378
+ const own = reach ?? {
379
+ access: String(computer?.values.access ?? "none"),
380
+ browser: computer?.values.browser === true,
381
+ };
382
+ for (const [position, one] of placed.entries())
383
+ if (isAgentsUnit(one.offer))
384
+ problems.push(...reachProblems(one.values, own).map((p) => ({ ...p, unit: position })));
385
+ return { placed, problems };
386
+ };
387
+ /** The connections a living agent has now; what is exposed to it is the Hub's to say. */
388
+ const toolsOf = async (agent) => (await hub.bindings.list({ actor: agent })).bindings.filter((binding) => binding.consumer === agent && binding.interface === TOOLS);
389
+ const placeOnAgent = async (input) => {
390
+ const record = recordOf(input.agent);
391
+ const taken = {
392
+ slots: new Set((await hub.bindings.list({ actor: input.agent })).bindings
393
+ .filter((binding) => binding.consumer === input.agent)
394
+ .map((binding) => binding.slot)),
395
+ skills: new Set(record.skills),
396
+ givers: record.units.map((unit) => ({
397
+ ref: unit.instance,
398
+ provider: unit.provider,
399
+ results: unit.results.map((result) => result.kind),
400
+ })),
401
+ };
402
+ const placing = await place(input.unit, input.needs ?? {}, taken, await catalog(), undefined);
403
+ if (placing.placed && isAgentsUnit(placing.placed.offer))
404
+ placing.problems.push(...reachProblems(placing.placed.values, reachOf(record)));
405
+ return placing;
406
+ };
407
+ const unwind = async (undo) => {
408
+ for (const step of undo.reverse())
409
+ await Promise.resolve()
410
+ .then(step)
411
+ .catch((failed) => log.error(`Undoing a creation: ${messageOf(failed)}`));
412
+ };
413
+ const closeAll = async (connections) => {
414
+ for (const { connection } of connections)
415
+ await hub.actors.close({ actorId: connection });
416
+ };
417
+ /** Run one placed unit for the agent, and give it what it gives. */
418
+ const run = async (work, placed, earlier) => {
419
+ const { offer, values, worked } = placed;
420
+ const { agent, record, undo } = work;
421
+ let made;
422
+ if (offer.call) {
423
+ const { call } = offer;
424
+ const needs = {};
425
+ const handles = {};
426
+ for (const [kind, ref] of Object.entries(placed.needs)) {
427
+ const giver = earlier(ref);
428
+ const handle = giver?.results.find((result) => result.kind === kind)?.handle;
429
+ if (!giver || !handle)
430
+ throw new Refused("failed", `The unit above didn't give ${kind} as it said it would.`);
431
+ needs[kind] = giver.instance;
432
+ handles[kind] = handle;
433
+ }
434
+ const applied = await call.apply({
435
+ unit: offer.unit.id,
436
+ values,
437
+ secrets: placed.secrets,
438
+ needs: handles,
439
+ agent,
440
+ name: `${work.name}: ${offer.unit.title}`.slice(0, MAX_NAME),
441
+ // Not for a child: a folder by a name it chose could be another agent's.
442
+ ...(record.parent ? {} : { agentName: work.name }),
443
+ });
444
+ undo.push(async () => {
445
+ try {
446
+ await call.remove({ instance: applied.instance });
447
+ }
448
+ catch (error) {
449
+ log.warn(`Removing a unit: ${messageOf(error)}`);
450
+ await closeAll(applied.connections);
451
+ }
452
+ });
453
+ const slots = applied.connections.map((one) => one.slot).sort();
454
+ if (slots.join() !== [...worked.slots].sort().join())
455
+ throw new Refused("failed", `${offer.unit.title} didn't give the connections it said it would.`);
456
+ const results = applied.results.map((result) => result.kind);
457
+ if (offer.unit.gives.results.some((kind) => !results.includes(kind)))
458
+ throw new Refused("failed", `${offer.unit.title} didn't give the results it said it would.`);
459
+ for (const { slot, connection, target } of applied.connections) {
460
+ await parts.exposeConnection(agent, connection, slot);
461
+ work.origins[connection] = {
462
+ provider: offer.provider,
463
+ kind: offer.unit.id,
464
+ target,
465
+ madeBy: work.caller,
466
+ madeFor: agent,
467
+ at: new Date().toISOString(),
468
+ };
469
+ }
470
+ made = {
471
+ instance: `uni_${crypto.randomUUID().replaceAll("-", "")}`,
472
+ provider: offer.provider,
473
+ providerInstance: applied.instance,
474
+ unit: offer.unit.id,
475
+ title: offer.unit.title,
476
+ values,
477
+ needs,
478
+ results: applied.results,
479
+ connections: applied.connections.map(({ slot, connection }) => ({ slot, connection })),
480
+ skills: worked.skills.map((skill) => skill.name),
481
+ ...(placed.viaMcp ? { viaMcp: true } : {}),
482
+ };
483
+ }
484
+ else if (offer.unit.id === CONNECTION_UNIT) {
485
+ const slot = values.slot;
486
+ await parts.exposeConnection(agent, values.connection, slot);
487
+ undo.push(() => hub.grants.unexpose({ to: agent, slot }));
488
+ }
489
+ else if (isAgentsUnit(offer)) {
490
+ await parts.exposeConnection(agent, self, AGENTS_SLOT);
491
+ undo.push(() => hub.grants.unexpose({ to: agent, slot: AGENTS_SLOT }));
492
+ const instance = `uni_${crypto.randomUUID().replaceAll("-", "")}`;
493
+ made = {
494
+ instance,
495
+ provider: self,
496
+ providerInstance: instance,
497
+ unit: offer.unit.id,
498
+ title: offer.unit.title,
499
+ values,
500
+ needs: {},
501
+ results: [],
502
+ connections: [{ slot: AGENTS_SLOT, connection: self }],
503
+ skills: worked.skills.map((skill) => skill.name),
504
+ };
505
+ }
506
+ for (const { name, files } of worked.skills) {
507
+ if (!library.has(name)) {
508
+ if (!files)
509
+ throw new Refused("unknown", `There is no skill ${name}.`);
510
+ const copied = {};
511
+ for (const path of files)
512
+ copied[path] = offer.call
513
+ ? (await offer.call.skill({ unit: offer.unit.id, name, path })).text
514
+ : skillFile(path);
515
+ library.add(copied, name);
516
+ // Another agent may have been given it meanwhile, and then it stays.
517
+ undo.push(() => {
518
+ if (![...Object.values(records.agents), ...creating.values()].some((one) => one.skills.includes(name)))
519
+ library.remove(name);
520
+ });
521
+ }
522
+ record.skills.push(name);
523
+ undo.push(() => {
524
+ record.skills = record.skills.filter((skill) => skill !== name);
525
+ });
526
+ }
527
+ if (made) {
528
+ const instance = made.instance;
529
+ record.units.push(made);
530
+ undo.push(() => {
531
+ record.units = record.units.filter((unit) => unit.instance !== instance);
532
+ });
533
+ }
534
+ return made;
535
+ };
536
+ const keepOrigins = (work) => {
537
+ Object.assign(records.connections, work.origins);
538
+ work.undo.push(() => {
539
+ for (const connection of Object.keys(work.origins))
540
+ delete records.connections[connection];
541
+ });
542
+ };
543
+ /** What a stack is, whichever way the caller said it. */
544
+ const stackOf = async (input) => {
545
+ if ("stack" in input)
546
+ return input.stack;
547
+ const template = templates().find((one) => one.id === input.template);
548
+ if (!template)
549
+ throw new Refused("unknown", `There is no template ${input.template}.`);
550
+ const fill = input.fill ?? [];
551
+ if (fill.length > template.units.length)
552
+ throw new Refused("invalid", `The template has ${template.units.length} units.`);
553
+ const offers = await catalog();
554
+ const base = await baseProviderNamed(template.base.provider);
555
+ return {
556
+ base: {
557
+ provider: base,
558
+ creatable: template.base.creatable,
559
+ answers: { ...template.base.answers, ...input.answers },
560
+ },
561
+ units: template.units.map((unit, position) => {
562
+ const offer = offerNamed(offers, unit.provider, unit.unit);
563
+ const filled = fill[position];
564
+ return {
565
+ provider: offer.provider,
566
+ unit: unit.unit,
567
+ values: { ...unit.values, ...filled?.values },
568
+ ...(unit.needs ? { needs: unit.needs } : {}),
569
+ ...(filled?.secrets ? { secrets: filled.secrets } : {}),
570
+ };
571
+ }),
572
+ template: template.id,
573
+ };
574
+ };
575
+ const baseProviderNamed = async (name) => {
576
+ for (const { actor } of parts.agentProviders())
577
+ if ((await parts.nameOf(actor)) === name)
578
+ return actor;
579
+ throw new Refused("unknown", `No connected agent provider is called ${name}.`);
580
+ };
581
+ const offerNamed = (offers, providerName, unit) => {
582
+ const offer = offers.find((one) => one.providerName === providerName && one.unit.id === unit);
583
+ if (!offer)
584
+ throw new Refused("unknown", `${providerName} offers no unit ${unit}.`);
585
+ return offer;
586
+ };
587
+ const templates = () => Object.values({
588
+ ...Object.fromEntries(parts.shipped.map((one) => [one.id, one])),
589
+ ...records.templates,
590
+ });
591
+ /** Remove a unit and what it made; a provider that has lost it leaves only the connections. */
592
+ const dropUnit = async (agent, record, unit) => {
593
+ if (unit.provider === self) {
594
+ await hub.grants.unexpose({ to: agent, slot: AGENTS_SLOT }).catch((error) => {
595
+ if (!alreadyGone(error))
596
+ throw error;
597
+ });
598
+ record.skills = record.skills.filter((skill) => !unit.skills.includes(skill));
599
+ record.units = record.units.filter((one) => one.instance !== unit.instance);
600
+ save();
601
+ return;
602
+ }
603
+ const provider = parts.unitProviders().find((one) => one.actor === unit.provider);
604
+ let removed = false;
605
+ try {
606
+ if (provider) {
607
+ await provider.call.remove({ instance: unit.providerInstance });
608
+ removed = true;
609
+ }
610
+ }
611
+ catch (error) {
612
+ if (!alreadyGone(error))
613
+ throw error;
614
+ }
615
+ if (!removed)
616
+ await closeAll(unit.connections);
617
+ for (const { connection } of unit.connections)
618
+ delete records.connections[connection];
619
+ record.skills = record.skills.filter((skill) => !unit.skills.includes(skill));
620
+ record.units = record.units.filter((one) => one.instance !== unit.instance);
621
+ save();
622
+ };
623
+ /** Which unit made a connection, if one did. */
624
+ const unitMaking = (connection) => {
625
+ for (const record of Object.values(records.agents))
626
+ for (const unit of record.units)
627
+ if (unit.connections.some((one) => one.connection === connection))
628
+ return unit;
629
+ return undefined;
630
+ };
631
+ const statusOf = async (unit) => unit.provider === self
632
+ ? { state: "connected", detail: undefined, connections: [] }
633
+ : parts
634
+ .unitProviders()
635
+ .find((one) => one.actor === unit.provider)
636
+ ?.call.status({ instance: unit.providerInstance });
637
+ const DOWN = { state: "down", detail: "Its provider can't be reached." };
638
+ const fromUnit = (record, given) => record.units.some((unit) => "slot" in given
639
+ ? unit.connections.some((one) => one.slot === given.slot)
640
+ : unit.skills.includes(given.skill));
641
+ return {
642
+ serial,
643
+ fromUnit,
644
+ unitMaking,
645
+ catalog,
646
+ allTemplates: templates,
647
+ baseProviderNamed,
648
+ /** How one connection a unit made stands, as its unit says. */
649
+ async connectionStatus(unit, connection) {
650
+ if (unit.provider === self)
651
+ return { state: "connected", tools: [] };
652
+ try {
653
+ const found = (await statusOf(unit))?.connections.find((one) => one.connection === connection);
654
+ if (found) {
655
+ const { connection: _, ...status } = found;
656
+ return status;
657
+ }
658
+ }
659
+ catch (error) {
660
+ log.warn(`A unit's status: ${messageOf(error)}`);
661
+ }
662
+ return { ...DOWN, tools: [] };
663
+ },
664
+ /** The agent's stack, top to bottom: its exposures as units, then what providers made. */
665
+ async read(agent, record, connectionStatus) {
666
+ const tools = await toolsOf(agent);
667
+ const instanceOfSkill = (name) => record.units.find((unit) => unit.skills.includes(name))?.instance ?? skillInstance(name);
668
+ const instanceOfSlot = (slot) => record.units.find((unit) => unit.connections.some((one) => one.slot === slot))?.instance ??
669
+ slotInstance(slot);
670
+ const own = (unit, title, instance, values, status) => ({ instance, provider: self, unit, title, values, needs: {}, status });
671
+ const units = [
672
+ ...record.skills
673
+ .filter((name) => instanceOfSkill(name) === skillInstance(name))
674
+ .map((name) => own(SKILL_UNIT, "Skill", skillInstance(name), { skill: name }, { state: "connected" })),
675
+ ...(await Promise.all(tools
676
+ .filter(({ slot }) => instanceOfSlot(slot) === slotInstance(slot))
677
+ .map(async ({ slot, provider }) => {
678
+ const { state, detail } = await connectionStatus(provider);
679
+ return own(MCP_UNIT, "MCP", slotInstance(slot), { [MCP_HOW]: CONNECTION_UNIT, connection: provider, slot }, { state, ...(detail ? { detail } : {}) });
680
+ }))),
681
+ ...(await Promise.all(record.units.map(async (unit) => {
682
+ let status = DOWN;
683
+ try {
684
+ const answer = await statusOf(unit);
685
+ if (answer)
686
+ status = {
687
+ state: answer.state,
688
+ ...(answer.detail ? { detail: answer.detail } : {}),
689
+ ...("surfaces" in answer && answer.surfaces ? { surfaces: answer.surfaces } : {}),
690
+ };
691
+ }
692
+ catch (error) {
693
+ log.warn(`A unit's status: ${messageOf(error)}`);
694
+ }
695
+ return {
696
+ instance: unit.instance,
697
+ ...(unit.viaMcp
698
+ ? {
699
+ provider: self,
700
+ unit: MCP_UNIT,
701
+ title: "MCP",
702
+ values: { [MCP_HOW]: unit.unit, ...unit.values },
703
+ }
704
+ : {
705
+ provider: unit.provider,
706
+ unit: unit.unit,
707
+ title: unit.title,
708
+ values: unit.values,
709
+ }),
710
+ needs: unit.needs,
711
+ status,
712
+ };
713
+ }))),
714
+ ];
715
+ return { units, tools, instanceOfSkill, instanceOfSlot };
716
+ },
717
+ async units() {
718
+ const [bases, offers] = await Promise.all([
719
+ parts.fromEach(parts.agentProviders(), async ({ actor, call }) => {
720
+ const providerName = await parts.nameOf(actor);
721
+ return (await call.creatable({})).creatables.map((one) => ({
722
+ ...one,
723
+ provider: actor,
724
+ providerName,
725
+ }));
726
+ }),
727
+ catalog(),
728
+ ]);
729
+ return {
730
+ bases: bases.flat(),
731
+ // What the MCP unit stands for isn't listed apart from it.
732
+ units: offers
733
+ .filter(({ unit, provider }) => !(provider === self ? unit.id === CONNECTION_UNIT : slotFieldOf(unit)))
734
+ .map(({ unit, provider, providerName }) => ({ ...unit, provider, providerName })),
735
+ };
736
+ },
737
+ templates: () => ({ templates: templates() }),
738
+ /** With `agent`, it's that agent's own: it replaces nothing, and starts no program. */
739
+ async saveTemplate(template, agent) {
740
+ if (agent && templates().some((one) => one.id === template.id))
741
+ throw new Refused("state", `There is a template ${template.id} already.`);
742
+ if (agent &&
743
+ Object.values(records.authors.templates).filter((one) => one === agent).length >=
744
+ AGENT_TEMPLATES)
745
+ throw new Refused("limit", `An agent can save at most ${AGENT_TEMPLATES} templates.`);
746
+ await baseProviderNamed(template.base.provider);
747
+ const offers = await catalog();
748
+ for (const unit of template.units) {
749
+ const { form } = offerNamed(offers, unit.provider, unit.unit).unit;
750
+ // A way not on offer now has fields no one can tell are secret, so it can't be kept.
751
+ const how = form.find((field) => field.id === MCP_HOW);
752
+ if (unit.unit === MCP_UNIT && !how?.options?.some((one) => one.value === unit.values?.how))
753
+ throw new Refused("unknown", `${String(unit.values?.how)} isn't on offer now.`);
754
+ for (const field of form) {
755
+ if (field.value === "secret" && unit.values?.[field.id] !== undefined)
756
+ throw new Refused("invalid", `${field.title} is a secret: a template holds none.`);
757
+ if (agent && field.authority === "runs" && unit.values?.[field.id] !== undefined)
758
+ throw new Refused("invalid", `An agent's template can't say what ${field.title} runs.`);
759
+ }
760
+ }
761
+ records.templates[template.id] = template;
762
+ // Saved by the person, it's the person's, whoever saved it first.
763
+ if (agent)
764
+ records.authors.templates[template.id] = agent;
765
+ else
766
+ delete records.authors.templates[template.id];
767
+ save();
768
+ return {};
769
+ },
770
+ removeTemplate({ id }, agent) {
771
+ if (!records.templates[id] || (agent && records.authors.templates[id] !== agent))
772
+ throw new Refused("unknown", `No saved template ${id}.`);
773
+ delete records.templates[id];
774
+ delete records.authors.templates[id];
775
+ save();
776
+ return {};
777
+ },
778
+ async check(input) {
779
+ const problems = [];
780
+ if ("stack" in input) {
781
+ await baseOf(input.stack.base).catch((error) => problems.push({ message: messageOf(error).slice(0, MAX_MESSAGE) }));
782
+ problems.push(...(await plan(input.stack.units)).problems);
783
+ }
784
+ else
785
+ problems.push(...(await placeOnAgent(input)).problems);
786
+ return { problems: problems.slice(0, MAX_PROBLEMS) };
787
+ },
788
+ /** All or none: what this made is undone, last first, if a step fails. */
789
+ async create(input, caller, child) {
790
+ const stack = await stackOf(input);
791
+ const { provider, creatable } = await baseOf(stack.base);
792
+ const { placed, problems } = await plan(stack.units, child?.reach);
793
+ if (problems.length)
794
+ throw refusal(problems);
795
+ const undo = [];
796
+ try {
797
+ const made = await provider.call.create({
798
+ creatable: creatable.id,
799
+ name: input.name,
800
+ answers: stack.base.answers ?? {},
801
+ });
802
+ const { agent } = made;
803
+ undo.push(() => parts.closing(agent, () => parts.removeAgent(agent, provider.actor)));
804
+ const record = {
805
+ provider: provider.actor,
806
+ creatable: creatable.id,
807
+ instructions: creatable.instructions,
808
+ skills: [],
809
+ units: [],
810
+ chat: made.chat,
811
+ ...(input.purpose ? { purpose: input.purpose } : {}),
812
+ ...(stack.template ? { template: stack.template } : {}),
813
+ ...(child ? { parent: child.parent } : {}),
814
+ ...(child?.computer ? { computer: child.computer } : {}),
815
+ };
816
+ creating.set(agent, record);
817
+ undo.push(() => creating.delete(agent));
818
+ await hub.grants.bind({
819
+ consumer: agent,
820
+ slot: made.settings,
821
+ provider: self,
822
+ export: "settings",
823
+ });
824
+ const work = { agent, name: input.name, caller, record, undo, origins: {} };
825
+ const applied = new Map();
826
+ for (const [position, one] of placed.entries()) {
827
+ const unit = await run(work, one, (ref) => applied.get(ref));
828
+ if (unit)
829
+ applied.set(position, unit);
830
+ }
831
+ if (child) {
832
+ for (const { slot, connection } of child.shares)
833
+ await parts.exposeConnection(agent, connection, slot);
834
+ for (const name of child.skills)
835
+ if (library.has(name) && !record.skills.includes(name))
836
+ record.skills.push(name);
837
+ // Its parent reaches its chat through management, which needs it for that.
838
+ await hub.grants.expose({ provider: agent, export: made.chat, to: self });
839
+ }
840
+ await hub.grants.expose({ provider: agent, export: made.chat, to: "person" });
841
+ records.agents[agent] = record;
842
+ undo.push(() => delete records.agents[agent]);
843
+ keepOrigins(work);
844
+ save();
845
+ creating.delete(agent);
846
+ return { agent };
847
+ }
848
+ catch (error) {
849
+ await unwind(undo);
850
+ throw error;
851
+ }
852
+ },
853
+ async addUnit(input, caller) {
854
+ const { agent } = input;
855
+ const record = recordOf(agent);
856
+ const { placed, problems } = await placeOnAgent(input);
857
+ if (problems.length || !placed)
858
+ throw refusal(problems);
859
+ const undo = [];
860
+ const work = {
861
+ agent,
862
+ name: await parts.nameOf(agent),
863
+ caller,
864
+ record,
865
+ undo,
866
+ origins: {},
867
+ };
868
+ try {
869
+ const unit = await run(work, placed, (ref) => record.units.find((one) => one.instance === ref));
870
+ keepOrigins(work);
871
+ save();
872
+ changed(agent);
873
+ if (unit)
874
+ return { instance: unit.instance };
875
+ return {
876
+ instance: placed.offer.unit.id === SKILL_UNIT
877
+ ? skillInstance(placed.values.skill)
878
+ : slotInstance(placed.values.slot),
879
+ };
880
+ }
881
+ catch (error) {
882
+ await unwind(undo);
883
+ throw error;
884
+ }
885
+ },
886
+ async removeUnit({ agent, instance }) {
887
+ const record = recordOf(agent);
888
+ const unit = record.units.find((one) => one.instance === instance);
889
+ if (unit) {
890
+ const needing = record.units.find((one) => Object.values(one.needs).includes(instance));
891
+ if (needing)
892
+ throw new Refused("state", `${needing.title} needs this unit.`);
893
+ await dropUnit(agent, record, unit);
894
+ changed(agent);
895
+ return {};
896
+ }
897
+ // What a provider's unit gave isn't a unit of its own, so it can't be taken off alone.
898
+ const skill = record.skills.find((name) => skillInstance(name) === instance && !fromUnit(record, { skill: name }));
899
+ if (skill) {
900
+ record.skills = record.skills.filter((name) => name !== skill);
901
+ save();
902
+ changed(agent);
903
+ return {};
904
+ }
905
+ const tool = (await toolsOf(agent)).find(({ slot }) => slotInstance(slot) === instance && !fromUnit(record, { slot }));
906
+ if (!tool)
907
+ throw new Refused("unknown", `${agent} has no unit ${instance}.`);
908
+ await hub.grants.unexpose({ to: agent, slot: tool.slot });
909
+ return {};
910
+ },
911
+ /** Every unit, last first: if one can't go, what is left can be tried again. */
912
+ async removeAll(agent, record) {
913
+ for (const unit of [...record.units].reverse())
914
+ await dropUnit(agent, record, unit);
915
+ },
916
+ };
917
+ }