@ai-ecoverse/slicc-agent 3.6.0 → 3.8.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.
Files changed (92) hide show
  1. package/README.md +20 -3
  2. package/bin/agent +76 -0
  3. package/dist/agent.js +7 -14
  4. package/dist/agent.js.map +2 -2
  5. package/dist/agents.js +133 -0
  6. package/dist/agents.js.map +7 -0
  7. package/dist/client.js +3 -1
  8. package/dist/client.js.map +2 -2
  9. package/dist/cone.js +2 -16
  10. package/dist/cone.js.map +2 -2
  11. package/dist/control.js +68 -10
  12. package/dist/control.js.map +2 -2
  13. package/dist/host.js +11 -2
  14. package/dist/host.js.map +2 -2
  15. package/dist/kernel/env.js +30 -3
  16. package/dist/kernel/env.js.map +2 -2
  17. package/dist/kernel/exec.js +2 -1
  18. package/dist/kernel/exec.js.map +2 -2
  19. package/dist/kernel/groups.js +31 -0
  20. package/dist/kernel/groups.js.map +7 -0
  21. package/dist/licks/index.js +16 -1
  22. package/dist/licks/index.js.map +2 -2
  23. package/dist/licks/lick.js.map +1 -1
  24. package/dist/licks/licks.js +12 -1
  25. package/dist/licks/licks.js.map +2 -2
  26. package/dist/main.js +38 -9
  27. package/dist/main.js.map +2 -2
  28. package/dist/roles/frontmatter.js +59 -0
  29. package/dist/roles/frontmatter.js.map +7 -0
  30. package/dist/roles/roles.js +204 -0
  31. package/dist/roles/roles.js.map +7 -0
  32. package/dist/scoops/cli.js +391 -0
  33. package/dist/scoops/cli.js.map +7 -0
  34. package/dist/scoops/extension.js +330 -0
  35. package/dist/scoops/extension.js.map +7 -0
  36. package/dist/scoops/from.js +5 -0
  37. package/dist/scoops/from.js.map +7 -0
  38. package/dist/scoops/identity.js +33 -0
  39. package/dist/scoops/identity.js.map +7 -0
  40. package/dist/scoops/index.js +120 -0
  41. package/dist/scoops/index.js.map +7 -0
  42. package/dist/scoops/requests.js +136 -0
  43. package/dist/scoops/requests.js.map +7 -0
  44. package/dist/scoops/service.js +419 -0
  45. package/dist/scoops/service.js.map +7 -0
  46. package/dist/scoops/sync.js +253 -0
  47. package/dist/scoops/sync.js.map +7 -0
  48. package/dist/scoops/visible.js +15 -0
  49. package/dist/scoops/visible.js.map +7 -0
  50. package/dist/services.js.map +2 -2
  51. package/dist/spectrum/agent-port.js +137 -54
  52. package/dist/spectrum/agent-port.js.map +2 -2
  53. package/dist/spectrum/messages.js +39 -16
  54. package/dist/spectrum/messages.js.map +2 -2
  55. package/dist/types/agent.d.ts +3 -1
  56. package/dist/types/agents.d.ts +54 -0
  57. package/dist/types/client.d.ts +3 -1
  58. package/dist/types/cone.d.ts +1 -3
  59. package/dist/types/control.d.ts +7 -1
  60. package/dist/types/host.d.ts +2 -1
  61. package/dist/types/kernel/client.d.ts +4 -0
  62. package/dist/types/kernel/env.d.ts +13 -2
  63. package/dist/types/kernel/exec.d.ts +2 -2
  64. package/dist/types/kernel/groups.d.ts +7 -0
  65. package/dist/types/licks/index.d.ts +5 -0
  66. package/dist/types/licks/lick.d.ts +1 -1
  67. package/dist/types/licks/licks.d.ts +2 -1
  68. package/dist/types/main.d.ts +3 -0
  69. package/dist/types/roles/frontmatter.d.ts +7 -0
  70. package/dist/types/roles/roles.d.ts +48 -0
  71. package/dist/types/scoops/cli.d.ts +66 -0
  72. package/dist/types/scoops/extension.d.ts +79 -0
  73. package/dist/types/scoops/from.d.ts +5 -0
  74. package/dist/types/scoops/identity.d.ts +6 -0
  75. package/dist/types/scoops/index.d.ts +37 -0
  76. package/dist/types/scoops/requests.d.ts +32 -0
  77. package/dist/types/scoops/service.d.ts +119 -0
  78. package/dist/types/scoops/sync.d.ts +22 -0
  79. package/dist/types/scoops/visible.d.ts +4 -0
  80. package/dist/types/services.d.ts +30 -0
  81. package/dist/types/spectrum/agent-port.d.ts +13 -11
  82. package/dist/types/spectrum/messages.d.ts +2 -1
  83. package/dist/types/views.d.ts +12 -0
  84. package/dist/views.js +89 -0
  85. package/dist/views.js.map +7 -0
  86. package/package.json +5 -3
  87. package/packages/vfs-root/agents/delegate.md +8 -0
  88. package/packages/vfs-root/agents/oracle.md +25 -0
  89. package/packages/vfs-root/agents/reviewer.md +23 -0
  90. package/packages/vfs-root/agents/scout.md +30 -0
  91. package/packages/vfs-root/agents/worker.md +23 -0
  92. package/packages/vfs-root/skills/agent/SKILL.md +72 -0
@@ -0,0 +1,253 @@
1
+ import { answerText } from "../agent.js";
2
+ import { resolve } from "../kernel/paths.js";
3
+ import {
4
+ choices,
5
+ fail,
6
+ flags,
7
+ isAnswer,
8
+ last,
9
+ MAX_DEPTH,
10
+ prompts,
11
+ readText,
12
+ role,
13
+ unsupported
14
+ } from "./cli.js";
15
+ import { SCOOPS_ROOT } from "./service.js";
16
+ const SESSIONS = { tmp: "/tmp/agent-sessions", durable: "/home/sessions" };
17
+ const SYNC_VALUES = [
18
+ "--name",
19
+ "--agent",
20
+ "--provider",
21
+ "--model",
22
+ "--thinking",
23
+ "--effort",
24
+ "--tools",
25
+ "--prompt",
26
+ "--file",
27
+ "--read-only",
28
+ "--system-prompt",
29
+ "--system-prompt-file",
30
+ "--background-after"
31
+ ];
32
+ const SYNC_SWITCHES = /* @__PURE__ */ new Set([
33
+ "--persist-session",
34
+ "--no-persist-session",
35
+ "--no-extensions",
36
+ "--no-skills",
37
+ "--no-context-files"
38
+ ]);
39
+ function persistOf(switches) {
40
+ if (switches.has("--no-persist-session")) return "none";
41
+ return switches.has("--persist-session") ? "durable" : "tmp";
42
+ }
43
+ function block(text) {
44
+ return typeof text === "string" ? text : JSON.stringify(text, null, 2);
45
+ }
46
+ function entryMarkdown(entry) {
47
+ const message = entry.model?.[0];
48
+ if (entry.kind === "pi.user") return `## Request
49
+
50
+ ${answerText(message?.content)}`;
51
+ if (entry.kind === "pi.tool-result")
52
+ return `### Result
53
+
54
+ \`\`\`
55
+ ${answerText(message?.content)}
56
+ \`\`\``;
57
+ if (entry.kind !== "pi.assistant") return void 0;
58
+ const parts = Array.isArray(message?.content) ? message.content : [];
59
+ const calls = parts.filter((part) => part.type === "toolCall").map((part) => `\`${part.name}\` ${block(part.arguments)}`);
60
+ return [`## Answer
61
+
62
+ ${answerText(message?.content)}`, ...calls].join("\n\n");
63
+ }
64
+ function transcript(record, entries) {
65
+ const body = entries.map(entryMarkdown).filter(Boolean);
66
+ return `# ${record.name}${record.role ? ` (${record.role})` : ""}
67
+
68
+ ${body.join("\n\n")}
69
+ `;
70
+ }
71
+ async function archive(deps, record, persist, context) {
72
+ if (persist === "none") return null;
73
+ const conversation = await deps.harness.conversation(
74
+ record.conversation,
75
+ context
76
+ );
77
+ if (!conversation) return null;
78
+ const { entries } = await conversation.context(context);
79
+ const path = `${SESSIONS[persist]}/${record.folder}.md`;
80
+ await deps.env.writeFile(path, transcript(record, entries), context);
81
+ return path;
82
+ }
83
+ async function finish(deps, id, persist, context) {
84
+ const record = deps.agents.state().scoops[id];
85
+ if (!record) return;
86
+ await archive(deps, record, persist, context).catch(() => null);
87
+ await deps.scoops.stop(id, false, context);
88
+ await deps.env.remove(
89
+ `${SCOOPS_ROOT}/${record.folder}`,
90
+ { recursive: true, force: true },
91
+ context
92
+ );
93
+ }
94
+ async function callerRunning(deps, who, context) {
95
+ if (!who.fromAgent) return false;
96
+ const state = deps.agents.state();
97
+ const conversation = who.scoop ? state.scoops[who.scoop]?.conversation : state.cones[who.cone]?.conversation;
98
+ return conversation !== void 0 && deps.scoops.busy(conversation, context);
99
+ }
100
+ async function keep(deps, id, tasks, target, context) {
101
+ if (target) await deps.scoops.detach(tasks, target, context);
102
+ await deps.agents.update((_tx, state) => {
103
+ const record = state.scoops[id];
104
+ if (record) record.kind = "async";
105
+ }, context);
106
+ }
107
+ function watchPid(deps, pid, end) {
108
+ const alive = deps.alive;
109
+ if (pid === null || !alive) return () => void 0;
110
+ const timer = setInterval(() => {
111
+ void alive(pid).then(
112
+ (yes) => yes || end("cancel"),
113
+ () => void 0
114
+ );
115
+ }, 1e3);
116
+ return () => clearInterval(timer);
117
+ }
118
+ async function plan(deps, request, parsed, context) {
119
+ const given = await prompts(deps, request.argv, request, context);
120
+ let workdir;
121
+ const notes = [];
122
+ if (!given.length && parsed.rest.length === 3) {
123
+ const [cwd, allowed, prompt] = parsed.rest;
124
+ workdir = resolve(request.cwd, cwd);
125
+ if (allowed && allowed !== "*")
126
+ notes.push(
127
+ `Use only these commands in bash: ${allowed}. This is not enforced; keep to it anyway.`
128
+ );
129
+ given.push(prompt);
130
+ } else if (parsed.rest.length) return fail(`unexpected argument ${parsed.rest[0]}`, 2);
131
+ if (!given.length)
132
+ return fail("agent needs --prompt, --file, - or <cwd> <allowed-commands> <prompt>", 2);
133
+ const systemFile = last(parsed, "--system-prompt-file");
134
+ const system = last(parsed, "--system-prompt") ?? (systemFile ? await readText(deps, systemFile, request, context) : void 0);
135
+ if (system) notes.push(system);
136
+ if (workdir) notes.push(`Your working folder is ${workdir}; you may change files there.`);
137
+ const readOnly = (last(parsed, "--read-only") ?? "").split(",").map((path) => path.trim()).filter(Boolean).map((path) => resolve(request.cwd, path));
138
+ const result = {
139
+ given,
140
+ instructions: notes.join("\n\n"),
141
+ readOnly,
142
+ persist: persistOf(parsed.switches)
143
+ };
144
+ if (workdir) result.workdir = workdir;
145
+ return result;
146
+ }
147
+ async function outcome(deps, tasks, context) {
148
+ let result = { text: "", failed: "no request", path: null };
149
+ for (const task of tasks) {
150
+ const settled = await deps.harness.waitForTask(task, context);
151
+ const outcome2 = settled.state.outcome;
152
+ result = outcome2.status === "completed" ? outcome2.result : { text: "", failed: outcome2.status, path: null };
153
+ }
154
+ return result;
155
+ }
156
+ function answerOf(result) {
157
+ if (result.failed) return fail(`the scoop's request ended without an answer: ${result.failed}`);
158
+ return { code: 0, out: result.text.endsWith("\n") ? result.text : `${result.text}
159
+ ` };
160
+ }
161
+ async function settle(deps, who, id, tasks, ending, context) {
162
+ const handle = id.slice("scoop:".length);
163
+ if (ending === "detach") {
164
+ await keep(deps, id, tasks, who.id, context);
165
+ const how = who.id ? "its answer arrives as a lick" : `follow it with agent wait ${handle}`;
166
+ return { code: 0, out: `${handle} continues in the background; ${how}
167
+ ` };
168
+ }
169
+ if (await callerRunning(deps, who, context)) {
170
+ await keep(deps, id, tasks, who.id, context);
171
+ return {
172
+ code: 143,
173
+ out: `agent: the call was interrupted; ${handle} continues and its answer arrives as a lick
174
+ `
175
+ };
176
+ }
177
+ return { code: 130, out: "agent: stopped\n" };
178
+ }
179
+ async function runSync(deps, request, who, context) {
180
+ const refused = unsupported(request.argv);
181
+ if (refused) return refused;
182
+ if (who.depth >= MAX_DEPTH) return fail(`agents nest at most ${MAX_DEPTH} deep`);
183
+ const parsed = flags(request.argv, SYNC_VALUES);
184
+ const unknown = [...parsed.switches].find((flag) => !SYNC_SWITCHES.has(flag));
185
+ if (unknown) return fail(`unknown option ${unknown}`, 2);
186
+ const planned = await plan(deps, request, parsed, context);
187
+ if (isAnswer(planned)) return planned;
188
+ const chosen = choices(parsed);
189
+ if (isAnswer(chosen)) return chosen;
190
+ const found = await role(deps, parsed);
191
+ if (found.error) return found.error;
192
+ const spawned = await deps.scoops.spawn(
193
+ {
194
+ cone: who.cone,
195
+ kind: "sync",
196
+ parent: who.scoop ?? who.cone,
197
+ depth: who.depth + 1,
198
+ roots: { write: planned.workdir ? [planned.workdir] : [], read: planned.readOnly },
199
+ ...planned.workdir ? { cwd: planned.workdir } : {},
200
+ ...planned.instructions ? { instructions: planned.instructions } : {},
201
+ target: null,
202
+ asker: who.id,
203
+ name: last(parsed, "--name") ?? found.role?.name ?? "agent",
204
+ ...found.role ? { role: found.role } : {},
205
+ ...chosen.model ? { model: chosen.model } : {},
206
+ ...chosen.thinking ? { thinking: chosen.thinking } : {},
207
+ ...chosen.tools ? { tools: chosen.tools } : {},
208
+ prompts: planned.given,
209
+ fromAgent: who.fromAgent,
210
+ request: request.id,
211
+ limits: found.roles.limits
212
+ },
213
+ context
214
+ );
215
+ if (spawned.code !== 0) return spawned;
216
+ const id = `scoop:${spawned.out.trim()}`;
217
+ const tasks = spawned.tasks ?? [];
218
+ let end = () => void 0;
219
+ const ended = new Promise((done) => {
220
+ end = done;
221
+ });
222
+ deps.sync.pending.set(request.id, { end });
223
+ const unwatch = watchPid(deps, request.pid, end);
224
+ try {
225
+ const raced = await Promise.race([outcome(deps, tasks, context), ended]);
226
+ if (typeof raced === "string") {
227
+ const answer = await settle(deps, who, id, tasks, raced, context);
228
+ if (answer.code === 130) await finish(deps, id, planned.persist, context);
229
+ return answer;
230
+ }
231
+ await finish(deps, id, planned.persist, context);
232
+ return answerOf(raced);
233
+ } finally {
234
+ unwatch();
235
+ deps.sync.pending.delete(request.id);
236
+ }
237
+ }
238
+ async function cancelSync(deps, requestId, _context) {
239
+ deps.sync.pending.get(requestId)?.end("cancel");
240
+ return { code: 0, out: "" };
241
+ }
242
+ async function detachSync(deps, requestId, _context) {
243
+ deps.sync.pending.get(requestId)?.end("detach");
244
+ return { code: 0, out: "" };
245
+ }
246
+ export {
247
+ SESSIONS,
248
+ cancelSync,
249
+ detachSync,
250
+ runSync,
251
+ transcript
252
+ };
253
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/scoops/sync.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ConversationId, EntryRecord, TaskId } from '@earendil-works/pi-durable';\nimport { answerText } from '../agent.ts';\nimport type { ScoopRecord } from '../agents.ts';\nimport { resolve } from '../kernel/paths.ts';\nimport {\n type Caller,\n choices,\n type Deps,\n fail,\n flags,\n isAnswer,\n last,\n MAX_DEPTH,\n prompts,\n readText,\n role,\n unsupported,\n} from './cli.ts';\nimport type { ReportResult } from './extension.ts';\nimport type { Request } from './requests.ts';\nimport { type Answer, SCOOPS_ROOT } from './service.ts';\n\nexport const SESSIONS = { tmp: '/tmp/agent-sessions', durable: '/home/sessions' };\n\ntype Ending = 'cancel' | 'detach';\n\ninterface Pending {\n end: (ending: Ending) => void;\n}\n\nexport interface SyncState {\n pending: Map<string, Pending>;\n}\n\nconst SYNC_VALUES = [\n '--name',\n '--agent',\n '--provider',\n '--model',\n '--thinking',\n '--effort',\n '--tools',\n '--prompt',\n '--file',\n '--read-only',\n '--system-prompt',\n '--system-prompt-file',\n '--background-after',\n];\nconst SYNC_SWITCHES = new Set([\n '--persist-session',\n '--no-persist-session',\n '--no-extensions',\n '--no-skills',\n '--no-context-files',\n]);\n\ntype Persist = 'tmp' | 'durable' | 'none';\n\nfunction persistOf(switches: Set<string>): Persist {\n if (switches.has('--no-persist-session')) return 'none';\n return switches.has('--persist-session') ? 'durable' : 'tmp';\n}\n\nfunction block(text: unknown): string {\n return typeof text === 'string' ? text : JSON.stringify(text, null, 2);\n}\n\nfunction entryMarkdown(entry: EntryRecord): string | undefined {\n const message = entry.model?.[0] as { content?: unknown; toolCallId?: string } | undefined;\n if (entry.kind === 'pi.user') return `## Request\\n\\n${answerText(message?.content)}`;\n if (entry.kind === 'pi.tool-result')\n return `### Result\\n\\n\\`\\`\\`\\n${answerText(message?.content)}\\n\\`\\`\\``;\n if (entry.kind !== 'pi.assistant') return undefined;\n const parts = Array.isArray(message?.content)\n ? (message.content as { type: string; name?: string; arguments?: unknown }[])\n : [];\n const calls = parts\n .filter((part) => part.type === 'toolCall')\n .map((part) => `\\`${part.name}\\` ${block(part.arguments)}`);\n return [`## Answer\\n\\n${answerText(message?.content)}`, ...calls].join('\\n\\n');\n}\n\nexport function transcript(record: ScoopRecord, entries: readonly EntryRecord[]): string {\n const body = entries.map(entryMarkdown).filter(Boolean);\n return `# ${record.name}${record.role ? ` (${record.role})` : ''}\\n\\n${body.join('\\n\\n')}\\n`;\n}\n\nasync function archive(\n deps: Deps,\n record: ScoopRecord,\n persist: Persist,\n context: Context\n): Promise<string | null> {\n if (persist === 'none') return null;\n const conversation = await deps.harness.conversation(\n record.conversation as ConversationId,\n context\n );\n if (!conversation) return null;\n const { entries } = await conversation.context(context);\n const path = `${SESSIONS[persist]}/${record.folder}.md`;\n await deps.env.writeFile(path, transcript(record, entries), context);\n return path;\n}\n\nasync function finish(deps: Deps, id: string, persist: Persist, context: Context): Promise<void> {\n const record = deps.agents.state().scoops[id];\n if (!record) return;\n await archive(deps, record, persist, context).catch(() => null);\n await deps.scoops.stop(id, false, context);\n await deps.env.remove(\n `${SCOOPS_ROOT}/${record.folder}`,\n { recursive: true, force: true },\n context\n );\n}\n\nasync function callerRunning(deps: Deps, who: Caller, context: Context): Promise<boolean> {\n if (!who.fromAgent) return false;\n const state = deps.agents.state();\n const conversation = who.scoop\n ? state.scoops[who.scoop]?.conversation\n : state.cones[who.cone]?.conversation;\n return conversation !== undefined && deps.scoops.busy(conversation, context);\n}\n\nasync function keep(\n deps: Deps,\n id: string,\n tasks: readonly number[],\n target: string | null,\n context: Context\n) {\n if (target) await deps.scoops.detach(tasks, target, context);\n await deps.agents.update((_tx, state) => {\n const record = state.scoops[id];\n if (record) record.kind = 'async';\n }, context);\n}\n\nfunction watchPid(deps: Deps, pid: number | null, end: (ending: Ending) => void): () => void {\n const alive = deps.alive;\n if (pid === null || !alive) return () => undefined;\n const timer = setInterval(() => {\n void alive(pid).then(\n (yes) => yes || end('cancel'),\n () => undefined\n );\n }, 1000);\n return () => clearInterval(timer);\n}\n\ninterface Plan {\n given: string[];\n workdir?: string;\n instructions: string;\n readOnly: string[];\n persist: Persist;\n}\n\nasync function plan(\n deps: Deps,\n request: Request,\n parsed: ReturnType<typeof flags>,\n context: Context\n) {\n const given = await prompts(deps, request.argv, request, context);\n let workdir: string | undefined;\n const notes: string[] = [];\n if (!given.length && parsed.rest.length === 3) {\n const [cwd, allowed, prompt] = parsed.rest as [string, string, string];\n workdir = resolve(request.cwd, cwd);\n if (allowed && allowed !== '*')\n notes.push(\n `Use only these commands in bash: ${allowed}. This is not enforced; keep to it anyway.`\n );\n given.push(prompt);\n } else if (parsed.rest.length) return fail(`unexpected argument ${parsed.rest[0]}`, 2);\n if (!given.length)\n return fail('agent needs --prompt, --file, - or <cwd> <allowed-commands> <prompt>', 2);\n const systemFile = last(parsed, '--system-prompt-file');\n const system =\n last(parsed, '--system-prompt') ??\n (systemFile ? await readText(deps, systemFile, request, context) : undefined);\n if (system) notes.push(system);\n if (workdir) notes.push(`Your working folder is ${workdir}; you may change files there.`);\n const readOnly = (last(parsed, '--read-only') ?? '')\n .split(',')\n .map((path) => path.trim())\n .filter(Boolean)\n .map((path) => resolve(request.cwd, path));\n const result: Plan = {\n given,\n instructions: notes.join('\\n\\n'),\n readOnly,\n persist: persistOf(parsed.switches),\n };\n if (workdir) result.workdir = workdir;\n return result;\n}\n\nasync function outcome(\n deps: Deps,\n tasks: readonly number[],\n context: Context\n): Promise<ReportResult> {\n let result: ReportResult = { text: '', failed: 'no request', path: null };\n for (const task of tasks) {\n const settled = await deps.harness.waitForTask(task as TaskId<ReportResult>, context);\n const outcome = settled.state.outcome;\n result =\n outcome.status === 'completed'\n ? outcome.result\n : { text: '', failed: outcome.status, path: null };\n }\n return result;\n}\n\nfunction answerOf(result: ReportResult): Answer {\n if (result.failed) return fail(`the scoop's request ended without an answer: ${result.failed}`);\n return { code: 0, out: result.text.endsWith('\\n') ? result.text : `${result.text}\\n` };\n}\n\nasync function settle(\n deps: Deps,\n who: Caller,\n id: string,\n tasks: readonly number[],\n ending: Ending,\n context: Context\n): Promise<Answer> {\n const handle = id.slice('scoop:'.length);\n if (ending === 'detach') {\n await keep(deps, id, tasks, who.id, context);\n const how = who.id ? 'its answer arrives as a lick' : `follow it with agent wait ${handle}`;\n return { code: 0, out: `${handle} continues in the background; ${how}\\n` };\n }\n if (await callerRunning(deps, who, context)) {\n await keep(deps, id, tasks, who.id, context);\n return {\n code: 143,\n out: `agent: the call was interrupted; ${handle} continues and its answer arrives as a lick\\n`,\n };\n }\n return { code: 130, out: 'agent: stopped\\n' };\n}\n\nexport async function runSync(\n deps: Deps,\n request: Request,\n who: Caller,\n context: Context\n): Promise<Answer> {\n const refused = unsupported(request.argv);\n if (refused) return refused;\n if (who.depth >= MAX_DEPTH) return fail(`agents nest at most ${MAX_DEPTH} deep`);\n const parsed = flags(request.argv, SYNC_VALUES);\n const unknown = [...parsed.switches].find((flag) => !SYNC_SWITCHES.has(flag));\n if (unknown) return fail(`unknown option ${unknown}`, 2);\n const planned = await plan(deps, request, parsed, context);\n if (isAnswer(planned)) return planned;\n const chosen = choices(parsed);\n if (isAnswer(chosen)) return chosen;\n const found = await role(deps, parsed);\n if (found.error) return found.error;\n const spawned = await deps.scoops.spawn(\n {\n cone: who.cone,\n kind: 'sync',\n parent: who.scoop ?? who.cone,\n depth: who.depth + 1,\n roots: { write: planned.workdir ? [planned.workdir] : [], read: planned.readOnly },\n ...(planned.workdir ? { cwd: planned.workdir } : {}),\n ...(planned.instructions ? { instructions: planned.instructions } : {}),\n target: null,\n asker: who.id,\n name: last(parsed, '--name') ?? found.role?.name ?? 'agent',\n ...(found.role ? { role: found.role } : {}),\n ...(chosen.model ? { model: chosen.model } : {}),\n ...(chosen.thinking ? { thinking: chosen.thinking as never } : {}),\n ...(chosen.tools ? { tools: chosen.tools } : {}),\n prompts: planned.given,\n fromAgent: who.fromAgent,\n request: request.id,\n limits: found.roles.limits,\n },\n context\n );\n if (spawned.code !== 0) return spawned;\n const id = `scoop:${spawned.out.trim()}`;\n const tasks = spawned.tasks ?? [];\n let end: (ending: Ending) => void = () => undefined;\n const ended = new Promise<Ending>((done) => {\n end = done;\n });\n deps.sync.pending.set(request.id, { end });\n const unwatch = watchPid(deps, request.pid, end);\n try {\n const raced = await Promise.race([outcome(deps, tasks, context), ended]);\n if (typeof raced === 'string') {\n const answer = await settle(deps, who, id, tasks, raced, context);\n if (answer.code === 130) await finish(deps, id, planned.persist, context);\n return answer;\n }\n await finish(deps, id, planned.persist, context);\n return answerOf(raced);\n } finally {\n unwatch();\n deps.sync.pending.delete(request.id);\n }\n}\n\nexport async function cancelSync(\n deps: Deps,\n requestId: string,\n _context: Context\n): Promise<Answer> {\n deps.sync.pending.get(requestId)?.end('cancel');\n return { code: 0, out: '' };\n}\n\nexport async function detachSync(\n deps: Deps,\n requestId: string,\n _context: Context\n): Promise<Answer> {\n deps.sync.pending.get(requestId)?.end('detach');\n return { code: 0, out: '' };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["outcome"]
7
+ }
@@ -0,0 +1,15 @@
1
+ async function visible(read, conversation, mark) {
2
+ let current = conversation;
3
+ let cap = Number.POSITIVE_INFINITY;
4
+ for (; ; ) {
5
+ if (current === mark.in) return mark.at <= cap;
6
+ const parent = (await read(current))?.parent;
7
+ if (!parent) return false;
8
+ cap = Math.min(cap, Number(parent.at));
9
+ current = Number(parent.conversationId);
10
+ }
11
+ }
12
+ export {
13
+ visible
14
+ };
15
+ //# sourceMappingURL=visible.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/scoops/visible.ts"],
4
+ "sourcesContent": ["import type { ConversationRecord } from '@earendil-works/pi-durable';\nimport type { Mark } from '../agents.ts';\n\nexport type ReadConversation = (id: number) => Promise<ConversationRecord | undefined>;\n\nexport async function visible(\n read: ReadConversation,\n conversation: number,\n mark: Mark\n): Promise<boolean> {\n let current = conversation;\n let cap = Number.POSITIVE_INFINITY;\n for (;;) {\n if (current === mark.in) return mark.at <= cap;\n const parent = (await read(current))?.parent;\n if (!parent) return false;\n cap = Math.min(cap, Number(parent.at));\n current = Number(parent.conversationId);\n }\n}\n"],
5
+ "mappings": "AAKA,eAAsB,QACpB,MACA,cACA,MACkB;AAClB,MAAI,UAAU;AACd,MAAI,MAAM,OAAO;AACjB,aAAS;AACP,QAAI,YAAY,KAAK,GAAI,QAAO,KAAK,MAAM;AAC3C,UAAM,UAAU,MAAM,KAAK,OAAO,IAAI;AACtC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,KAAK,IAAI,KAAK,OAAO,OAAO,EAAE,CAAC;AACrC,cAAU,OAAO,OAAO,cAAc;AAAA,EACxC;AACF;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/services.ts"],
4
- "sourcesContent": ["import { type Context, defineService, type ReplicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\n\nexport type SendMode = 'steer' | 'followUp' | 'reject';\n\nexport interface SendRequest {\n text: string;\n whenBusy: SendMode;\n requestId: string | null;\n}\n\nexport interface OperationError {\n code: string;\n message: string;\n}\n\nexport type SendResponse =\n | { accepted: true; submissionId: string; error: null }\n | { accepted: false; submissionId: null; error: OperationError };\n\nexport type PromptResult =\n | { status: 'done'; text: string; reason: null }\n | { status: 'unanswered'; text: null; reason: string };\n\nexport interface AgentControl {\n send(request: SendRequest, context: Context): Promise<SendResponse>;\n wait(submissionId: string, context: Context): Promise<PromptResult>;\n withdraw(\n submissionId: string,\n context: Context\n ): Promise<{ outcome: 'withdrawn' | 'already_placed' | 'not_found' }>;\n abort(context: Context): Promise<void>;\n compact(instructions: string | null, context: Context): Promise<SendResponse>;\n reset(handoff: string | null, context: Context): Promise<void>;\n configure(change: AgentSettingsChange, context: Context): Promise<void>;\n rewind(messageId: string | null, context: Context): Promise<Rewound>;\n resolveLick(\n lickId: string,\n state: 'confirmed' | 'dismissed',\n context: Context\n ): Promise<LickResolved>;\n webhook(\n name: string,\n delivery: WebhookRequest,\n context: Context\n ): Promise<{ delivered: boolean }>;\n}\n\nexport type LickResolved =\n | { done: true; text: string; error: null }\n | { done: false; text: null; error: string };\n\nexport interface WebhookRequest {\n id: string | null;\n headers: Record<string, string>;\n body: string;\n}\n\nexport type Rewound =\n | { done: true; text: string; reason: null }\n | { done: false; text: null; reason: 'busy' | 'no-turn' };\n\nexport interface AgentSettingsChange {\n model: { provider: string; modelId: string } | null;\n thinkingLevel: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null;\n}\n\nexport const AgentControl = defineService<AgentControl>('slicc.agent.control');\n\nexport type Delivered = 'run' | 'steer' | 'follow-up';\n\nexport interface AgentTranscript {\n readonly state: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n}\n\nexport const AgentTranscript = defineService<AgentTranscript>('slicc.agent.transcript');\n\nexport interface AgentSessions {\n attach(sessionId: string, context: Context): Promise<void>;\n detach(context: Context): Promise<void>;\n}\n\nexport const AgentSessions = defineService<AgentSessions>('slicc.agent.sessions');\n\nexport const AGENT_SESSION = 'agent';\n\nexport interface ModelChoice {\n id: string;\n label: string;\n provider: string;\n kind: 'chat' | 'classifier';\n reasoning: boolean;\n contextWindow: number;\n}\n\nexport interface AccountState {\n id: string;\n provider: string;\n identity: string;\n status: 'connected' | 'disconnected';\n auth: 'api-key' | 'oauth';\n needs: 'cors-free-transport' | null;\n}\n\nexport interface BudgetState {\n provider: string;\n percent: number;\n window: 'weekly';\n resets: string;\n}\n\nexport interface SignIn {\n clientId: string;\n scopes: string;\n imsEnvironment: string;\n}\n\nexport interface SettingsState {\n models: ModelChoice[];\n accounts: AccountState[];\n budget?: BudgetState | null;\n}\n\nexport interface AgentSettings {\n readonly state: ReplicatedState<SettingsState>;\n connect(\n providerId: string,\n secret: string,\n region: string | null,\n context: Context\n ): Promise<void>;\n disconnect(providerId: string, context: Context): Promise<void>;\n signIn(providerId: string, context: Context): Promise<SignIn | null>;\n}\n\nexport const AgentSettings = defineService<AgentSettings>('slicc.agent.settings');\n"],
5
- "mappings": "AAAA,SAAuB,qBAA2C;AAmE3D,MAAM,eAAe,cAA4B,qBAAqB;AAStE,MAAM,kBAAkB,cAA+B,wBAAwB;AAO/E,MAAM,gBAAgB,cAA6B,sBAAsB;AAEzE,MAAM,gBAAgB;AAmDtB,MAAM,gBAAgB,cAA6B,sBAAsB;",
4
+ "sourcesContent": ["import { type Context, defineService, type ReplicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\n\nexport type SendMode = 'steer' | 'followUp' | 'reject';\n\nexport interface SendRequest {\n text: string;\n whenBusy: SendMode;\n requestId: string | null;\n agentId?: string | null;\n}\n\nexport interface OperationError {\n code: string;\n message: string;\n}\n\nexport type SendResponse =\n | { accepted: true; submissionId: string; error: null }\n | { accepted: false; submissionId: null; error: OperationError };\n\nexport type PromptResult =\n | { status: 'done'; text: string; reason: null }\n | { status: 'unanswered'; text: null; reason: string };\n\nexport interface AgentControl {\n send(request: SendRequest, context: Context): Promise<SendResponse>;\n wait(submissionId: string, context: Context): Promise<PromptResult>;\n withdraw(\n submissionId: string,\n context: Context\n ): Promise<{ outcome: 'withdrawn' | 'already_placed' | 'not_found' }>;\n abort(context: Context): Promise<void>;\n compact(instructions: string | null, context: Context): Promise<SendResponse>;\n reset(handoff: string | null, context: Context): Promise<void>;\n configure(change: AgentSettingsChange, context: Context): Promise<void>;\n rewind(messageId: string | null, context: Context): Promise<Rewound>;\n resolveLick(\n lickId: string,\n state: 'confirmed' | 'dismissed',\n context: Context\n ): Promise<LickResolved>;\n webhook(\n name: string,\n delivery: WebhookRequest,\n context: Context\n ): Promise<{ delivered: boolean }>;\n stopAgent(agentId: string, context: Context): Promise<void>;\n selectCone(agentId: string, context: Context): Promise<void>;\n createCone(name: string, context: Context): Promise<Created>;\n createScoop(parentId: string, name: string, context: Context): Promise<Created>;\n drop(agentId: string, context: Context): Promise<Created>;\n unqueue(\n agentId: string | null,\n submissionId: string,\n context: Context\n ): Promise<{ outcome: 'withdrawn' | 'already_placed' | 'not_found' }>;\n}\n\nexport type Created = { id: string; error: null } | { id: null; error: string };\n\nexport interface AgentSummary {\n id: string;\n name: string;\n kind: 'cone' | 'scoop';\n parentId: string | null;\n role: string | null;\n}\n\nexport interface AgentsSummary {\n active: string;\n agents: AgentSummary[];\n}\n\nexport type LickResolved =\n | { done: true; text: string; error: null }\n | { done: false; text: null; error: string };\n\nexport interface WebhookRequest {\n id: string | null;\n headers: Record<string, string>;\n body: string;\n}\n\nexport type Rewound =\n | { done: true; text: string; reason: null }\n | { done: false; text: null; reason: 'busy' | 'no-turn' };\n\nexport interface AgentSettingsChange {\n model: { provider: string; modelId: string } | null;\n thinkingLevel: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null;\n agentId?: string | null;\n}\n\nexport const AgentControl = defineService<AgentControl>('slicc.agent.control');\n\nexport type Delivered = 'run' | 'steer' | 'follow-up';\n\nexport interface AgentTranscript {\n readonly state: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents: ReplicatedState<AgentsSummary>;\n readonly views: ReplicatedState<Record<string, ConversationView>>;\n}\n\nexport const AgentTranscript = defineService<AgentTranscript>('slicc.agent.transcript');\n\nexport interface AgentSessions {\n attach(sessionId: string, context: Context): Promise<void>;\n detach(context: Context): Promise<void>;\n}\n\nexport const AgentSessions = defineService<AgentSessions>('slicc.agent.sessions');\n\nexport const AGENT_SESSION = 'agent';\n\nexport interface ModelChoice {\n id: string;\n label: string;\n provider: string;\n kind: 'chat' | 'classifier';\n reasoning: boolean;\n contextWindow: number;\n}\n\nexport interface AccountState {\n id: string;\n provider: string;\n identity: string;\n status: 'connected' | 'disconnected';\n auth: 'api-key' | 'oauth';\n needs: 'cors-free-transport' | null;\n}\n\nexport interface BudgetState {\n provider: string;\n percent: number;\n window: 'weekly';\n resets: string;\n}\n\nexport interface SignIn {\n clientId: string;\n scopes: string;\n imsEnvironment: string;\n}\n\nexport interface SettingsState {\n models: ModelChoice[];\n accounts: AccountState[];\n budget?: BudgetState | null;\n}\n\nexport interface AgentSettings {\n readonly state: ReplicatedState<SettingsState>;\n connect(\n providerId: string,\n secret: string,\n region: string | null,\n context: Context\n ): Promise<void>;\n disconnect(providerId: string, context: Context): Promise<void>;\n signIn(providerId: string, context: Context): Promise<SignIn | null>;\n}\n\nexport const AgentSettings = defineService<AgentSettings>('slicc.agent.settings');\n"],
5
+ "mappings": "AAAA,SAAuB,qBAA2C;AA8F3D,MAAM,eAAe,cAA4B,qBAAqB;AAWtE,MAAM,kBAAkB,cAA+B,wBAAwB;AAO/E,MAAM,gBAAgB,cAA6B,sBAAsB;AAEzE,MAAM,gBAAgB;AAmDtB,MAAM,gBAAgB,cAA6B,sBAAsB;",
6
6
  "names": []
7
7
  }
@@ -23,73 +23,107 @@ function status(view, messages) {
23
23
  class AgentAdapter extends Emitter {
24
24
  #connection;
25
25
  #ready;
26
- #messages = [];
26
+ #messages = /* @__PURE__ */ new Map();
27
+ #active = null;
28
+ #creating = /* @__PURE__ */ new Map();
27
29
  constructor(connection) {
28
30
  super();
29
31
  this.#connection = connection;
30
32
  this.#ready = new Promise((resolve) => {
31
- connection.transcript.subscribe((view) => {
32
- this.#update(view);
33
+ connection.transcript.subscribe(() => {
34
+ this.#refresh();
33
35
  resolve();
34
36
  });
35
- connection.deliveries.subscribe(() => {
36
- const view = this.#view();
37
- if (view) this.#update(view);
38
- });
37
+ connection.deliveries.subscribe(() => this.#refresh());
38
+ connection.views?.subscribe(() => this.#refresh());
39
+ connection.agents?.subscribe(() => this.#refresh());
39
40
  });
40
41
  }
41
- #view() {
42
- return this.#connection.transcript.value;
42
+ #summary() {
43
+ const agents = this.#connection.agents?.value?.agents;
44
+ return agents?.length ? agents : [{ id: CONE, name: "sliccy", kind: "cone", parentId: null, role: null }];
45
+ }
46
+ #view(agentId = this.active()) {
47
+ const views = this.#connection.views?.value;
48
+ const view = views?.[agentId];
49
+ if (view) return view;
50
+ const cone = this.#connection.agents?.value?.active ?? CONE;
51
+ return agentId === cone ? this.#connection.transcript.value : void 0;
43
52
  }
44
- #update(view) {
45
- const before = this.#messages;
46
- this.#messages = toMessages(view, this.#connection.deliveries.value);
53
+ #refresh() {
47
54
  this.emit("agents", this.list());
48
- if (before.length !== this.#messages.length) {
49
- this.emit("messages", CONE);
55
+ for (const summary of this.#summary()) this.#update(summary.id);
56
+ for (const id of [...this.#messages.keys()])
57
+ if (!this.#summary().some((summary) => summary.id === id)) this.#messages.delete(id);
58
+ }
59
+ #update(agentId) {
60
+ const view = this.#view(agentId);
61
+ if (!view) return;
62
+ const before = this.#messages.get(agentId) ?? [];
63
+ const after = toMessages(view, this.#connection.deliveries.value);
64
+ this.#messages.set(agentId, after);
65
+ if (before.length !== after.length) {
66
+ this.emit("messages", agentId);
50
67
  return;
51
68
  }
52
- const changed = this.#messages.filter(
69
+ const changed = after.filter(
53
70
  (message, index) => JSON.stringify(message) !== JSON.stringify(before[index])
54
71
  );
55
- if (changed.length === 1)
56
- this.emit("message", { agentId: CONE, message: changed[0] });
57
- else if (changed.length > 1) this.emit("messages", CONE);
72
+ if (changed.length === 1) this.emit("message", { agentId, message: changed[0] });
73
+ else if (changed.length > 1) this.emit("messages", agentId);
58
74
  }
59
75
  ready() {
60
76
  return this.#ready;
61
77
  }
62
78
  list() {
63
- const doc = this.#view()?.docs["pi.agent"];
64
- return [
65
- {
66
- id: CONE,
67
- name: "sliccy",
68
- kind: "cone",
69
- parentId: null,
70
- status: status(this.#view(), this.#messages),
79
+ return this.#summary().map((summary) => {
80
+ const view = this.#view(summary.id);
81
+ const doc = view?.docs["pi.agent"];
82
+ return {
83
+ id: summary.id,
84
+ name: summary.role ? `${summary.name} \xB7 ${summary.role}` : summary.name,
85
+ kind: summary.kind,
86
+ parentId: summary.parentId,
87
+ status: status(view, this.#messages.get(summary.id) ?? []),
71
88
  model: doc?.model ? `${doc.model.provider}/${doc.model.modelId}` : "",
72
89
  contextFill: 0,
73
90
  unread: 0,
74
91
  thinking: thinking[doc?.thinkingLevel ?? "off"] ?? "off"
75
- }
76
- ];
92
+ };
93
+ });
77
94
  }
78
95
  active() {
79
- return CONE;
80
- }
81
- select() {
96
+ const known = this.#summary();
97
+ if (this.#active && this.#creating.has(this.#active)) return this.#active;
98
+ if (this.#active && known.some((summary) => summary.id === this.#active)) return this.#active;
99
+ return this.#connection.agents?.value?.active ?? CONE;
100
+ }
101
+ select(id) {
102
+ if (this.#creating.has(id)) {
103
+ this.#active = id;
104
+ this.emit("active", id);
105
+ return;
106
+ }
107
+ const found = this.#summary().find((summary) => summary.id === id);
108
+ if (!found) return;
109
+ this.#active = id;
110
+ if (found.kind === "cone") void this.#connection.control.selectCone(id, BACKGROUND_CONTEXT);
111
+ this.emit("active", id);
112
+ this.emit("messages", id);
82
113
  }
83
- messages() {
84
- return this.#messages;
114
+ messages(agentId = this.active()) {
115
+ return this.#messages.get(agentId) ?? [];
85
116
  }
86
- async send(_agentId, input) {
117
+ async send(agentId, input) {
87
118
  const outgoing = typeof input === "string" ? { text: input } : input;
119
+ const target = this.#creating.has(agentId) ? await this.#creating.get(agentId) : agentId;
120
+ if (target === null) throw new Error("The scoop could not be created.");
88
121
  const sent = await this.#connection.control.send(
89
122
  {
90
123
  text: outgoing.text,
91
124
  whenBusy: outgoing.mode === "queue" ? "followUp" : "steer",
92
- requestId: crypto.randomUUID()
125
+ requestId: crypto.randomUUID(),
126
+ agentId: target || null
93
127
  },
94
128
  BACKGROUND_CONTEXT
95
129
  );
@@ -99,26 +133,28 @@ class AgentAdapter extends Emitter {
99
133
  const rewound = await this.#connection.control.rewind(messageId, BACKGROUND_CONTEXT);
100
134
  return rewound.done ? { text: rewound.text } : null;
101
135
  }
102
- stop() {
103
- void this.#connection.control.abort(BACKGROUND_CONTEXT);
136
+ stop(agentId = this.active()) {
137
+ void this.#connection.control.stopAgent(agentId, BACKGROUND_CONTEXT);
104
138
  }
105
- busy() {
106
- return isBusy(this.#view());
139
+ busy(agentId = this.active()) {
140
+ return isBusy(this.#view(agentId));
107
141
  }
108
- queue() {
109
- return queued(this.#view());
142
+ queue(agentId = this.active()) {
143
+ return queued(this.#view(agentId));
110
144
  }
111
- unqueue(_agentId, messageId) {
112
- void this.#connection.control.withdraw(messageId.replace(/^q/, ""), BACKGROUND_CONTEXT);
145
+ unqueue(agentId, messageId) {
146
+ void this.#connection.control.unqueue(agentId, messageId.replace(/^q/, ""), BACKGROUND_CONTEXT);
113
147
  }
114
148
  suggestion() {
115
149
  return null;
116
150
  }
117
151
  answer() {
118
152
  }
119
- resolveLick(_agentId, messageId, state) {
120
- const message = this.#messages.find((candidate) => candidate.id === messageId);
121
- const entry = this.#view()?.entries.find((candidate) => `e${candidate.id}` === messageId);
153
+ resolveLick(agentId, messageId, state) {
154
+ const message = this.messages(agentId).find((candidate) => candidate.id === messageId);
155
+ const entry = this.#view(agentId)?.entries.find(
156
+ (candidate) => `e${candidate.id}` === messageId
157
+ );
122
158
  const lick = message?.role === "lick" && entry ? entryLick(entry) : void 0;
123
159
  if (lick) void this.#connection.control.resolveLick(lick.id, state, BACKGROUND_CONTEXT);
124
160
  }
@@ -128,19 +164,25 @@ class AgentAdapter extends Emitter {
128
164
  clear() {
129
165
  void this.#connection.control.reset(null, BACKGROUND_CONTEXT);
130
166
  }
131
- #configure(change) {
167
+ #configure(agentId, change) {
132
168
  void this.#connection.control.configure(
133
- { model: change.model ?? null, thinkingLevel: change.thinkingLevel ?? null },
169
+ {
170
+ model: change.model ?? null,
171
+ thinkingLevel: change.thinkingLevel ?? null,
172
+ agentId
173
+ },
134
174
  BACKGROUND_CONTEXT
135
175
  );
136
176
  }
137
- setModel(_agentId, model) {
177
+ setModel(agentId, model) {
138
178
  const at = model.indexOf("/");
139
179
  if (at <= 0) return;
140
- this.#configure({ model: { provider: model.slice(0, at), modelId: model.slice(at + 1) } });
180
+ this.#configure(agentId, {
181
+ model: { provider: model.slice(0, at), modelId: model.slice(at + 1) }
182
+ });
141
183
  }
142
- setThinking(_agentId, level) {
143
- this.#configure({ thinkingLevel: level });
184
+ setThinking(agentId, level) {
185
+ this.#configure(agentId, { thinkingLevel: level });
144
186
  }
145
187
  async older() {
146
188
  return [];
@@ -148,8 +190,49 @@ class AgentAdapter extends Emitter {
148
190
  commands() {
149
191
  return [];
150
192
  }
151
- createScoop() {
152
- throw new Error("Scoops come with a later slicc-agent");
193
+ createScoop(parentId, name) {
194
+ const provisional = `scoop:creating-${crypto.randomUUID()}`;
195
+ const created = this.#connection.control.createScoop(parentId, name, BACKGROUND_CONTEXT).then(
196
+ (result) => result.id,
197
+ () => null
198
+ );
199
+ this.#creating.set(provisional, created);
200
+ void created.then((id) => {
201
+ if (this.#active !== provisional) return;
202
+ this.#active = id ?? parentId;
203
+ this.emit("active", this.#active);
204
+ this.emit("messages", this.#active);
205
+ });
206
+ return {
207
+ id: provisional,
208
+ name,
209
+ kind: "scoop",
210
+ parentId,
211
+ status: "idle",
212
+ model: "",
213
+ contextFill: 0,
214
+ unread: 0
215
+ };
216
+ }
217
+ async drop(agentId) {
218
+ const parent = this.#summary().find((summary) => summary.id === agentId)?.parentId ?? CONE;
219
+ const dropped = await this.#connection.control.drop(agentId, BACKGROUND_CONTEXT);
220
+ if (dropped.error !== null) throw new Error(dropped.error);
221
+ if (this.#active === agentId) this.select(parent);
222
+ }
223
+ async createCone(name) {
224
+ const created = await this.#connection.control.createCone(name, BACKGROUND_CONTEXT);
225
+ if (created.error !== null) throw new Error(created.error);
226
+ return {
227
+ id: created.id,
228
+ name: name.trim(),
229
+ kind: "cone",
230
+ parentId: null,
231
+ status: "idle",
232
+ model: "",
233
+ contextFill: 0,
234
+ unread: 0
235
+ };
153
236
  }
154
237
  frozen() {
155
238
  return [];