@vascend/talos 0.1.4 → 0.1.6

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 (251) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +37 -12
  3. package/dist/core/agent.d.ts +16 -3
  4. package/dist/core/agent.js +170 -70
  5. package/dist/core/background.d.ts +10 -3
  6. package/dist/core/background.js +55 -9
  7. package/dist/core/bootstrap.js +1 -0
  8. package/dist/core/compaction.js +1 -1
  9. package/dist/core/config-storage.d.ts +5 -0
  10. package/dist/core/config-storage.js +83 -0
  11. package/dist/core/config.d.ts +11 -2
  12. package/dist/core/config.js +24 -8
  13. package/dist/core/context-ledger.js +2 -1
  14. package/dist/core/context-policy.d.ts +1 -1
  15. package/dist/core/context-policy.js +10 -7
  16. package/dist/core/credentials.js +29 -30
  17. package/dist/core/effort.d.ts +1 -1
  18. package/dist/core/effort.js +5 -1
  19. package/dist/core/execution-guidance.d.ts +1 -1
  20. package/dist/core/execution-guidance.js +2 -0
  21. package/dist/core/file-coordination.d.ts +3 -0
  22. package/dist/core/file-coordination.js +28 -0
  23. package/dist/core/file-lease-store.d.ts +3 -0
  24. package/dist/core/file-lease-store.js +28 -12
  25. package/dist/core/fusion.d.ts +24 -0
  26. package/dist/core/fusion.js +80 -0
  27. package/dist/core/model-limits.d.ts +1 -0
  28. package/dist/core/model-limits.js +5 -1
  29. package/dist/core/permission-queue.js +15 -15
  30. package/dist/core/request-plan.js +4 -3
  31. package/dist/core/session-naming.d.ts +3 -0
  32. package/dist/core/session-naming.js +47 -0
  33. package/dist/core/session-usage.js +4 -1
  34. package/dist/core/session.d.ts +5 -2
  35. package/dist/core/session.js +22 -7
  36. package/dist/core/settings.d.ts +2 -0
  37. package/dist/core/settings.js +5 -16
  38. package/dist/core/step-policy.d.ts +18 -0
  39. package/dist/core/step-policy.js +6 -0
  40. package/dist/core/storage-health.d.ts +24 -0
  41. package/dist/core/storage-health.js +72 -0
  42. package/dist/core/tool-artifacts.d.ts +2 -0
  43. package/dist/core/tool-artifacts.js +21 -3
  44. package/dist/core/tool-result-index.js +29 -33
  45. package/dist/core/tool-result-store.js +6 -5
  46. package/dist/core/types.d.ts +26 -2
  47. package/dist/core/user-inbox.d.ts +9 -0
  48. package/dist/core/user-inbox.js +22 -3
  49. package/dist/core/user-input.js +1 -1
  50. package/dist/fusion/autopilot-fast.d.ts +59 -0
  51. package/dist/fusion/autopilot-fast.js +213 -0
  52. package/dist/fusion/autopilot.d.ts +75 -0
  53. package/dist/fusion/autopilot.js +308 -0
  54. package/dist/fusion/focus.d.ts +30 -0
  55. package/dist/fusion/focus.js +122 -0
  56. package/dist/fusion/gate.d.ts +30 -0
  57. package/dist/fusion/gate.js +95 -0
  58. package/dist/fusion/jev.d.ts +71 -0
  59. package/dist/fusion/jev.js +164 -0
  60. package/dist/fusion/receipts.d.ts +25 -0
  61. package/dist/fusion/receipts.js +34 -0
  62. package/dist/fusion/route.d.ts +25 -0
  63. package/dist/fusion/route.js +84 -0
  64. package/dist/fusion/run.d.ts +4 -0
  65. package/dist/fusion/run.js +186 -0
  66. package/dist/horizon/engine.d.ts +20 -1
  67. package/dist/horizon/engine.js +57 -0
  68. package/dist/horizon/index.d.ts +2 -2
  69. package/dist/horizon/index.js +1 -1
  70. package/dist/horizon/types.d.ts +29 -0
  71. package/dist/index.js +12 -0
  72. package/dist/providers/anthropic-oauth.d.ts +8 -3
  73. package/dist/providers/anthropic-oauth.js +13 -10
  74. package/dist/providers/anthropic.d.ts +9 -2
  75. package/dist/providers/anthropic.js +64 -21
  76. package/dist/providers/cli-models.d.ts +30 -0
  77. package/dist/providers/cli-models.js +194 -0
  78. package/dist/providers/model-effort.js +1 -1
  79. package/dist/providers/openai-oauth.d.ts +8 -3
  80. package/dist/providers/openai-oauth.js +9 -3
  81. package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
  82. package/dist/providers/openrouter-manual-fallback.js +20 -20
  83. package/dist/providers/registry.js +3 -2
  84. package/dist/providers/retry.d.ts +3 -0
  85. package/dist/providers/retry.js +44 -1
  86. package/dist/remote/cifratura.d.ts +12 -0
  87. package/dist/remote/cifratura.js +66 -0
  88. package/dist/remote/index.d.ts +15 -0
  89. package/dist/remote/index.js +65 -0
  90. package/dist/remote/relay-client.d.ts +58 -0
  91. package/dist/remote/relay-client.js +315 -0
  92. package/dist/remote/sessione-remota.d.ts +60 -0
  93. package/dist/remote/sessione-remota.js +266 -0
  94. package/dist/sdk.d.ts +7 -2
  95. package/dist/sdk.js +3 -1
  96. package/dist/tools/builtin.js +41 -20
  97. package/dist/tools/capabilities.d.ts +3 -1
  98. package/dist/tools/capabilities.js +5 -2
  99. package/dist/tools/computer-use.js +2 -1
  100. package/dist/tools/pagination.js +1 -1
  101. package/dist/tools/program.js +34 -21
  102. package/dist/tools/team-tools.js +11 -6
  103. package/dist/tui/account-entry.js +14 -9
  104. package/dist/tui/agent-label.d.ts +2 -0
  105. package/dist/tui/agent-label.js +5 -0
  106. package/dist/tui/agent-sessions-screen.d.ts +35 -0
  107. package/dist/tui/agent-sessions-screen.js +178 -0
  108. package/dist/tui/agent-sessions.d.ts +53 -0
  109. package/dist/tui/agent-sessions.js +236 -0
  110. package/dist/tui/bottom-input.d.ts +57 -2
  111. package/dist/tui/bottom-input.js +449 -54
  112. package/dist/tui/branding.js +8 -4
  113. package/dist/tui/capability-lists.d.ts +10 -0
  114. package/dist/tui/capability-lists.js +41 -0
  115. package/dist/tui/command-output-registry.js +4 -0
  116. package/dist/tui/command-transcript.d.ts +1 -1
  117. package/dist/tui/command-transcript.js +17 -21
  118. package/dist/tui/commands.js +6 -2
  119. package/dist/tui/compaction-status.d.ts +7 -0
  120. package/dist/tui/compaction-status.js +39 -0
  121. package/dist/tui/config-recovery.d.ts +12 -0
  122. package/dist/tui/config-recovery.js +54 -0
  123. package/dist/tui/context-meter.d.ts +3 -0
  124. package/dist/tui/context-meter.js +35 -8
  125. package/dist/tui/diff.d.ts +9 -0
  126. package/dist/tui/diff.js +43 -19
  127. package/dist/tui/fusion-menu.d.ts +5 -0
  128. package/dist/tui/fusion-menu.js +89 -0
  129. package/dist/tui/i18n-catalog.js +351 -2
  130. package/dist/tui/live-command.d.ts +1 -0
  131. package/dist/tui/live-command.js +9 -0
  132. package/dist/tui/live-output.d.ts +1 -0
  133. package/dist/tui/live-output.js +11 -0
  134. package/dist/tui/logo.js +3 -0
  135. package/dist/tui/model-details.js +13 -12
  136. package/dist/tui/model-picker.d.ts +2 -0
  137. package/dist/tui/model-picker.js +59 -0
  138. package/dist/tui/model-selection.js +19 -10
  139. package/dist/tui/native-scrollback.d.ts +2 -1
  140. package/dist/tui/native-scrollback.js +31 -6
  141. package/dist/tui/prompt-queue-store.d.ts +1 -0
  142. package/dist/tui/prompt-queue-store.js +4 -0
  143. package/dist/tui/prompt-queue.d.ts +3 -0
  144. package/dist/tui/prompt-queue.js +19 -1
  145. package/dist/tui/provider-label.d.ts +2 -0
  146. package/dist/tui/provider-label.js +9 -0
  147. package/dist/tui/provider-menu.js +25 -24
  148. package/dist/tui/queue-screen.js +2 -1
  149. package/dist/tui/queued-questions.js +2 -4
  150. package/dist/tui/quick-help.d.ts +1 -1
  151. package/dist/tui/quick-help.js +3 -0
  152. package/dist/tui/remote-command.d.ts +7 -0
  153. package/dist/tui/remote-command.js +79 -0
  154. package/dist/tui/repl.d.ts +11 -2
  155. package/dist/tui/repl.js +457 -327
  156. package/dist/tui/review-store.d.ts +8 -1
  157. package/dist/tui/review-store.js +64 -11
  158. package/dist/tui/review-team.js +4 -3
  159. package/dist/tui/runtime-error.d.ts +2 -0
  160. package/dist/tui/runtime-error.js +26 -0
  161. package/dist/tui/scrollback-archive.d.ts +16 -0
  162. package/dist/tui/scrollback-archive.js +90 -0
  163. package/dist/tui/selector-mouse.d.ts +1 -0
  164. package/dist/tui/selector-mouse.js +1 -1
  165. package/dist/tui/selector.d.ts +11 -0
  166. package/dist/tui/selector.js +28 -4
  167. package/dist/tui/session-controls.d.ts +26 -0
  168. package/dist/tui/session-controls.js +65 -0
  169. package/dist/tui/session-naming-notice.d.ts +14 -0
  170. package/dist/tui/session-naming-notice.js +59 -0
  171. package/dist/tui/session-picker.d.ts +2 -0
  172. package/dist/tui/session-picker.js +32 -0
  173. package/dist/tui/session-replay.d.ts +1 -0
  174. package/dist/tui/session-replay.js +101 -32
  175. package/dist/tui/session-transcript-layout.d.ts +9 -0
  176. package/dist/tui/session-transcript-layout.js +41 -0
  177. package/dist/tui/startup.js +63 -33
  178. package/dist/tui/status-line.d.ts +1 -1
  179. package/dist/tui/status-line.js +20 -12
  180. package/dist/tui/storage-warning.d.ts +2 -0
  181. package/dist/tui/storage-warning.js +35 -0
  182. package/dist/tui/talos-art-data.d.ts +1 -0
  183. package/dist/tui/talos-art-data.js +2 -0
  184. package/dist/tui/talos-art.d.ts +1 -0
  185. package/dist/tui/talos-art.js +9 -0
  186. package/dist/tui/task-labels.d.ts +1 -0
  187. package/dist/tui/task-labels.js +8 -0
  188. package/dist/tui/terminal-text.d.ts +2 -1
  189. package/dist/tui/terminal-text.js +28 -2
  190. package/dist/tui/theme.js +2 -2
  191. package/dist/tui/tool-presentation.d.ts +3 -0
  192. package/dist/tui/tool-presentation.js +98 -0
  193. package/dist/tui/tool-style.js +4 -0
  194. package/dist/tui/transcript-store.d.ts +25 -0
  195. package/dist/tui/transcript-store.js +151 -0
  196. package/dist/tui/usage-footer.d.ts +1 -1
  197. package/dist/tui/usage-footer.js +3 -1
  198. package/dist/tui/vascend-id.d.ts +49 -0
  199. package/dist/tui/vascend-id.js +321 -0
  200. package/dist/tui/web-login-screen.js +4 -3
  201. package/dist/tui/web-login.d.ts +7 -15
  202. package/dist/tui/web-login.js +38 -78
  203. package/dist/tui/worker-transcripts.d.ts +31 -0
  204. package/dist/tui/worker-transcripts.js +91 -0
  205. package/dist/vascend/candidate-collector.d.ts +6 -5
  206. package/dist/vascend/candidate-collector.js +44 -43
  207. package/dist/vascend/context-feedback-loop.js +2 -0
  208. package/dist/vascend/context-lint.js +32 -2
  209. package/dist/vascend/context-market-selection.js +12 -5
  210. package/dist/vascend/context-pack-render.js +10 -4
  211. package/dist/vascend/context-packet-mirrors.d.ts +8 -0
  212. package/dist/vascend/context-packet-mirrors.js +24 -0
  213. package/dist/vascend/context-plan-events.d.ts +4 -0
  214. package/dist/vascend/context-plan-events.js +44 -0
  215. package/dist/vascend/context-snapshot.d.ts +1 -0
  216. package/dist/vascend/context-snapshot.js +8 -5
  217. package/dist/vascend/context-store-reader.d.ts +20 -0
  218. package/dist/vascend/context-store-reader.js +46 -0
  219. package/dist/vascend/context-window-compiler.d.ts +1 -0
  220. package/dist/vascend/context-window-compiler.js +1 -0
  221. package/dist/vascend/file-lock.d.ts +1 -0
  222. package/dist/vascend/file-lock.js +26 -0
  223. package/dist/vascend/granular-kingdom.d.ts +1 -0
  224. package/dist/vascend/granular-kingdom.js +52 -17
  225. package/dist/vascend/kingdom-bus.d.ts +1 -0
  226. package/dist/vascend/kingdom-bus.js +3 -0
  227. package/dist/vascend/kingdom-channel.d.ts +2 -0
  228. package/dist/vascend/kingdom-channel.js +3 -0
  229. package/dist/vascend/kingdom-live.d.ts +32 -4
  230. package/dist/vascend/kingdom-live.js +149 -13
  231. package/dist/vascend/kingdom-run.d.ts +5 -1
  232. package/dist/vascend/kingdom-run.js +6 -2
  233. package/dist/vascend/kingdom.d.ts +3 -0
  234. package/dist/vascend/kingdom.js +22 -11
  235. package/dist/vascend/memory-packet.d.ts +2 -1
  236. package/dist/vascend/memory-packet.js +31 -44
  237. package/dist/vascend/memory-store.d.ts +1 -0
  238. package/dist/vascend/memory-store.js +12 -4
  239. package/dist/vascend/plan-store.d.ts +3 -1
  240. package/dist/vascend/plan-store.js +6 -4
  241. package/dist/vascend/room-memory.d.ts +13 -2
  242. package/dist/vascend/room-memory.js +48 -6
  243. package/dist/vascend/runtime.d.ts +3 -11
  244. package/dist/vascend/runtime.js +11 -123
  245. package/dist/vascend/session-search.js +2 -0
  246. package/dist/vascend/team-session.js +3 -2
  247. package/dist/vascend/vision.d.ts +3 -1
  248. package/dist/vascend/vision.js +6 -4
  249. package/dist/vascend/worker-session.d.ts +20 -0
  250. package/dist/vascend/worker-session.js +83 -0
  251. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { runTurn } from "../core/agent.js";
2
2
  import { Session } from "../core/session.js";
3
- import { createUserInbox } from "../core/user-inbox.js";
4
- import { kingdomLive } from "./kingdom-live.js";
3
+ import { createWorkerInbox } from "./kingdom-live.js";
4
+ import { createWorkerSession } from "./worker-session.js";
5
5
  import { disjointAreas } from "./team-areas.js";
6
6
  import { fileActor, withFileActor } from "../core/file-coordination.js";
7
7
  import { launchGranular, granularConcurrency, formatGranularReport, } from "./granular-launcher.js";
@@ -39,6 +39,8 @@ function nestedTaskFields(slug) {
39
39
  function nestedActivity(slug, event) {
40
40
  if (event.type === "kingdom")
41
41
  return event;
42
+ if (event.type === "usage")
43
+ return { ...event, record: { ...event.record, actor: event.record.actor ?? slug } };
42
44
  if (event.type === "turn_start")
43
45
  return {
44
46
  type: "kingdom", phase: "vassal", slug, state: "active", label: "Pianifica il task", ...nestedTaskFields(slug),
@@ -63,9 +65,13 @@ function nestedActivity(slug, event) {
63
65
  };
64
66
  }
65
67
  if (event.type === "tool_call")
66
- return {
67
- type: "kingdom", phase: "vassal", slug, state: "active", label: event.call.name, ...nestedTaskFields(slug),
68
- };
68
+ return { type: "kingdom", phase: "tool", slug,
69
+ callId: event.call.id, tool: event.call.name, args: JSON.stringify(event.call.input).slice(0, 2400),
70
+ state: "start", at: Date.now() };
71
+ if (event.type === "tool_result")
72
+ return { type: "kingdom", phase: "tool", slug,
73
+ callId: event.result.callId, tool: "", output: event.result.output.slice(0, 12_000),
74
+ state: event.result.isError ? "error" : "ok", at: Date.now() };
69
75
  if (event.type === "done")
70
76
  return {
71
77
  type: "kingdom", phase: "vassal", slug, state: "done", ...nestedTaskFields(slug),
@@ -108,7 +114,7 @@ OBIETTIVO_UNITA'
108
114
  }
109
115
  // Costruisce il TaskRunner che esegue una unita' come sessione di agente.
110
116
  export function makeAgentUnitRunner(opts) {
111
- const { harness, kingdomId, castleSlug, signal, confirm, cwd, isolated } = opts;
117
+ const { harness, kingdomId, castleSlug, confirm, cwd, isolated } = opts;
112
118
  const peers = opts.units.map((u) => u.id);
113
119
  const depth = opts.depth ?? 2;
114
120
  const maxDepth = maxTeamDepth(opts);
@@ -144,6 +150,8 @@ export function makeAgentUnitRunner(opts) {
144
150
  ...(cwd ? { cwd } : {}),
145
151
  };
146
152
  const self = `${castleSlug}/${unit.id}`;
153
+ let controller = new AbortController();
154
+ const signal = controller.signal;
147
155
  const spawnKingdom = depth < maxDepth ? async (specs) => {
148
156
  const units = nestedUnits(specs);
149
157
  if (!units.length)
@@ -173,8 +181,16 @@ export function makeAgentUnitRunner(opts) {
173
181
  ...(route[1] ? { taskRef: route[1] } : {}),
174
182
  });
175
183
  }
184
+ const textBuffers = new Map();
185
+ const flushText = (slug) => {
186
+ const text = textBuffers.get(slug)?.trim();
187
+ textBuffers.delete(slug);
188
+ if (text)
189
+ ctx.emit({ type: "kingdom", phase: "say", slug, text: text.slice(0, 12_000), at: Date.now() });
190
+ };
176
191
  const report = await launchGranularKingdom({
177
192
  ...opts,
193
+ signal: controller.signal,
178
194
  harness: { ...harness, config: runConfig, tools: workerTools },
179
195
  castleSlug: self,
180
196
  units,
@@ -182,7 +198,21 @@ export function makeAgentUnitRunner(opts) {
182
198
  maxDepth,
183
199
  markCastle: undefined,
184
200
  onEvent: (id, event) => {
185
- const mapped = nestedActivity(`${self}/${id}`, event);
201
+ const slug = `${self}/${id}`;
202
+ // Preserve raw child events for the host before the compact parent summary.
203
+ if (event.type !== "kingdom" && event.type !== "usage")
204
+ opts.onEvent?.(`${unit.id}/${id}`, event);
205
+ if (event.type === "text") {
206
+ const text = (textBuffers.get(slug) ?? "") + event.delta;
207
+ textBuffers.set(slug, text);
208
+ if (text.length >= 480)
209
+ flushText(slug);
210
+ return;
211
+ }
212
+ if (event.type === "tool_call" || event.type === "turn_end" || event.type === "done" || event.type === "error"
213
+ || (event.type === "kingdom" && event.phase === "session" && event.state === "despawned"))
214
+ flushText(slug);
215
+ const mapped = nestedActivity(slug, event);
186
216
  if (mapped)
187
217
  ctx.emit(mapped);
188
218
  },
@@ -192,13 +222,14 @@ export function makeAgentUnitRunner(opts) {
192
222
  } : undefined;
193
223
  // Anche il sotto-collaboratore ha la sua casella: la scheda laterale non
194
224
  // distingue i livelli, e nemmeno chi ci scrive dentro deve farlo.
195
- const inbox = createUserInbox();
196
- kingdomLive.register(kingdomId, self, inbox);
225
+ const binding = createWorkerInbox();
226
+ const inbox = binding.inbox;
197
227
  const deps = {
198
228
  provider: resolved.provider,
199
229
  tools: workerTools,
200
230
  session,
201
231
  config: runConfig,
232
+ env: harness.env,
202
233
  confirm,
203
234
  ask: opts.ask,
204
235
  host: undefined,
@@ -227,18 +258,22 @@ export function makeAgentUnitRunner(opts) {
227
258
  + (unit.assignment?.instructions ? `\nISTRUZIONI_AGGIUNTIVE\n${unit.assignment.instructions}\n` : "")
228
259
  + (ctx.prerequisites?.length ? "\nPREREQUISITI_COMPLETATI (risultati da verificare)\n"
229
260
  + ctx.prerequisites.map((result) => `[${result.id}] ${result.summary}`).join("\n").slice(0, 6000) + "\n" : "") + unit.goal;
261
+ const worker = createWorkerSession({ kingdomId, slug: self, sessionId: session.id, goal: unit.goal,
262
+ parentSignal: ctx.signal, controller, binding, deps, sink, retain: !isolated ? opts.retainSessions ?? false : false,
263
+ onController: next => { controller = next; } });
230
264
  try {
231
- await runTurn(deps, input, sink, ctx.signal);
265
+ sink({ type: "kingdom", phase: "session", kingdomId, sessionId: session.id,
266
+ slug: self, state: "spawned", goal: unit.goal, at: Date.now() });
267
+ await runTurn(deps, input, sink, signal);
268
+ if (signal.aborted)
269
+ throw new Error("interrotta");
270
+ if (runError || !completed)
271
+ throw new Error(runError || "unita' terminata senza completamento verificato");
272
+ return { summary: summary.trim() };
232
273
  }
233
274
  finally {
234
- deps.fileCoordination?.hub.clearActor(deps.fileCoordination.actor.id);
235
- kingdomLive.release(kingdomId, self);
275
+ worker.finish();
236
276
  }
237
- if (ctx.signal.aborted)
238
- throw new Error("interrotta");
239
- if (runError || !completed)
240
- throw new Error(runError || "unita' terminata senza completamento verificato");
241
- return { summary: summary.trim() };
242
277
  };
243
278
  }
244
279
  // Endpoint completo: lancia le unita' granulari di un castello in parallelo.
@@ -19,6 +19,7 @@ export declare class KingdomBus {
19
19
  private seq;
20
20
  post(kingdomId: string, from: string, to: string | null, text: string, ts: number): KingdomMessage;
21
21
  inbox(kingdomId: string, me: string, filter?: (message: KingdomMessage) => boolean): KingdomMessage[];
22
+ peekInbox(kingdomId: string, me: string): readonly KingdomMessage[];
22
23
  history(kingdomId: string): readonly KingdomMessage[];
23
24
  clear(kingdomId: string): void;
24
25
  }
@@ -41,6 +41,9 @@ export class KingdomBus {
41
41
  return read.messages;
42
42
  }
43
43
  // Tutti i messaggi del regno (per il riepilogo/log), senza toccare i cursori.
44
+ peekInbox(kingdomId, me) {
45
+ return consumeInbox(this.boards.get(kingdomId) ?? [], me, this.cursors.get(`${kingdomId}::${me}`) ?? { last: 0 }).messages;
46
+ }
44
47
  history(kingdomId) {
45
48
  return this.boards.get(kingdomId) ?? [];
46
49
  }
@@ -8,6 +8,7 @@ export declare class PersistentKingdomChannel {
8
8
  constructor(sessionDir: string, opts?: KingdomChannelOptions);
9
9
  post(kingdomId: string, from: string, to: string | null, text: string, ts: number): KingdomMessage;
10
10
  inbox(kingdomId: string, me: string, filter?: (message: KingdomMessage) => boolean): KingdomMessage[];
11
+ peekInbox(kingdomId: string, me: string): readonly KingdomMessage[];
11
12
  history(kingdomId: string): readonly KingdomMessage[];
12
13
  clear(kingdomId: string): void;
13
14
  }
@@ -15,6 +16,7 @@ export declare function getKingdomChannel(sessionDir: string, opts?: KingdomChan
15
16
  export interface KingdomBusLike {
16
17
  post(kingdomId: string, from: string, to: string | null, text: string, ts: number): KingdomMessage;
17
18
  inbox(kingdomId: string, me: string, filter?: (message: KingdomMessage) => boolean): KingdomMessage[];
19
+ peekInbox(kingdomId: string, me: string): readonly KingdomMessage[];
18
20
  history(kingdomId: string): readonly KingdomMessage[];
19
21
  clear(kingdomId: string): void;
20
22
  }
@@ -127,6 +127,9 @@ export class PersistentKingdomChannel {
127
127
  });
128
128
  }
129
129
  // Tutti i messaggi del regno (per riepilogo/log), senza toccare i cursori.
130
+ peekInbox(kingdomId, me) {
131
+ return consumeInbox(readBoard(busFile(this.sessionDir, kingdomId)), me, readCursors(cursorsFile(this.sessionDir, kingdomId))[me] ?? { last: 0 }).messages;
132
+ }
130
133
  history(kingdomId) {
131
134
  return readBoard(busFile(this.sessionDir, kingdomId));
132
135
  }
@@ -1,9 +1,20 @@
1
- import type { UserInbox } from "../core/user-inbox.js";
2
- export interface LiveVassal {
1
+ import { type PendingUserMessage, type UserInbox } from "../core/user-inbox.js";
2
+ import type { ContentPart } from "../core/types.js";
3
+ export interface LiveVassalOptions {
4
+ readonly contextSource?: Pick<import("../core/agent.js").AgentDeps, "config" | "env" | "session" | "tools">;
5
+ readonly stop?: () => void;
6
+ readonly wake?: () => void;
7
+ readonly bindInbox?: (inbox: UserInbox) => void;
8
+ readonly close?: () => void;
9
+ }
10
+ export interface LiveVassal extends LiveVassalOptions {
3
11
  readonly kingdomId: string;
4
12
  readonly slug: string;
5
13
  readonly inbox: UserInbox;
6
14
  readonly since: number;
15
+ readonly stop?: () => void;
16
+ stopping?: boolean;
17
+ active?: boolean;
7
18
  }
8
19
  export interface DeliveryOutcome {
9
20
  readonly ok: boolean;
@@ -12,9 +23,14 @@ export interface DeliveryOutcome {
12
23
  }
13
24
  export declare class KingdomLive {
14
25
  private readonly vassals;
15
- register(kingdomId: string, slug: string, inbox: UserInbox, now?: number): void;
26
+ register(kingdomId: string, slug: string, inbox: UserInbox, now?: number, options?: LiveVassalOptions): void;
16
27
  release(kingdomId: string, slug: string): void;
17
28
  has(kingdomId: string, slug: string): boolean;
29
+ reachable(kingdomId: string, slug: string): boolean;
30
+ contextSource(kingdomId: string, slug: string): LiveVassalOptions["contextSource"];
31
+ setActive(kingdomId: string, slug: string, active: boolean): void;
32
+ bindInbox(kingdomId: string, slug: string, inbox: UserInbox): DeliveryOutcome;
33
+ wake(kingdomId: string, slug: string): DeliveryOutcome;
18
34
  /** Slug dei collaboratori che stanno lavorando adesso in questo regno. */
19
35
  slugs(kingdomId: string): readonly string[];
20
36
  /**
@@ -22,7 +38,19 @@ export declare class KingdomLive {
22
38
  * letto: ritorna l'id, cosi' l'host puo' dire "in attesa" e poi "letto"
23
39
  * quando arriva l'evento `user_message` di quel vassallo.
24
40
  */
25
- deliver(kingdomId: string, slug: string, text: string, messageId?: string): DeliveryOutcome;
41
+ deliver(kingdomId: string, slug: string, text: string, messageId?: string, parts?: readonly ContentPart[], immediate?: boolean): DeliveryOutcome;
42
+ /** Accoda la compattazione reale; runTurn la esegue al prossimo confine sicuro. */
43
+ compact(kingdomId: string, slug: string, instructions?: string): DeliveryOutcome;
44
+ /** Richiede l'arresto del solo worker. Il finally del runtime rilascia il registro. */
45
+ stop(kingdomId: string, slug: string): DeliveryOutcome;
26
46
  clear(kingdomId: string): void;
47
+ clearAll(): void;
27
48
  }
28
49
  export declare const kingdomLive: KingdomLive;
50
+ /** Casella stabile del loop, anche quando l'host collega la sua coda interattiva. */
51
+ export declare function createWorkerInbox(): {
52
+ inbox: UserInbox;
53
+ bind: (inbox: UserInbox) => void;
54
+ take: () => PendingUserMessage | undefined;
55
+ dispose: () => void;
56
+ };
@@ -8,54 +8,190 @@
8
8
  // chat principale), che l'agente svuota da solo al primo confine sicuro, subito
9
9
  // dopo gli esiti dei tool.
10
10
  //
11
- // Registro in-memory di processo, chiave `${kingdomId}::${slug}`. Vive quanto il
12
- // turno del vassallo: chi lo registra lo rilascia in un finally, altrimenti la
13
- // UI crederebbe di poter scrivere a un agente che non c'e' piu'.
11
+ // Registro in-memory di processo, chiave `${kingdomId}::${slug}`. Il runtime
12
+ // rilascia i worker effimeri al finally; gli host possono conservare sessioni
13
+ // idle con un vero controller di ripresa, fino a clear o abort del padre.
14
+ import { createUserInbox } from "../core/user-inbox.js";
14
15
  const key = (kingdomId, slug) => `${kingdomId}::${slug}`;
15
16
  export class KingdomLive {
16
17
  vassals = new Map();
17
- register(kingdomId, slug, inbox, now = Date.now()) {
18
+ register(kingdomId, slug, inbox, now = Date.now(), options = {}) {
18
19
  const id = String(kingdomId || "").trim();
19
20
  const who = String(slug || "").trim();
20
21
  if (!id || !who)
21
22
  return;
22
- this.vassals.set(key(id, who), { kingdomId: id, slug: who, inbox, since: now });
23
+ this.release(id, who);
24
+ this.vassals.set(key(id, who), { kingdomId: id, slug: who, inbox, since: now, active: true, ...options });
23
25
  }
24
26
  release(kingdomId, slug) {
25
- this.vassals.delete(key(String(kingdomId || "").trim(), String(slug || "").trim()));
27
+ const id = key(String(kingdomId || "").trim(), String(slug || "").trim());
28
+ const live = this.vassals.get(id);
29
+ this.vassals.delete(id);
30
+ live?.close?.();
26
31
  }
27
32
  has(kingdomId, slug) {
33
+ return Boolean(this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()))?.active);
34
+ }
35
+ reachable(kingdomId, slug) {
28
36
  return this.vassals.has(key(String(kingdomId || "").trim(), String(slug || "").trim()));
29
37
  }
38
+ contextSource(kingdomId, slug) {
39
+ return this.vassals.get(key(kingdomId, slug))?.contextSource;
40
+ }
41
+ setActive(kingdomId, slug, active) {
42
+ const live = this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()));
43
+ if (live) {
44
+ live.active = active;
45
+ live.stopping = false;
46
+ }
47
+ }
48
+ bindInbox(kingdomId, slug, inbox) {
49
+ const live = this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()));
50
+ if (!live)
51
+ return { ok: false, error: "collaboratore non piu' attivo" };
52
+ if (!live.bindInbox)
53
+ return { ok: false, error: "coda esterna non disponibile" };
54
+ try {
55
+ live.bindInbox(inbox);
56
+ return { ok: true };
57
+ }
58
+ catch (err) {
59
+ return { ok: false, error: err.message };
60
+ }
61
+ }
62
+ wake(kingdomId, slug) {
63
+ const live = this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()));
64
+ if (!live || live.stopping)
65
+ return { ok: false, error: "collaboratore non piu' attivo" };
66
+ try {
67
+ live.wake?.();
68
+ return { ok: true };
69
+ }
70
+ catch (err) {
71
+ return { ok: false, error: err.message };
72
+ }
73
+ }
30
74
  /** Slug dei collaboratori che stanno lavorando adesso in questo regno. */
31
75
  slugs(kingdomId) {
32
76
  const id = String(kingdomId || "").trim();
33
- return [...this.vassals.values()].filter((v) => v.kingdomId === id).map((v) => v.slug);
77
+ return [...this.vassals.values()].filter((v) => v.kingdomId === id && v.active).map((v) => v.slug);
34
78
  }
35
79
  /**
36
80
  * Consegna un messaggio dell'utente a un collaboratore. Non attende che sia
37
81
  * letto: ritorna l'id, cosi' l'host puo' dire "in attesa" e poi "letto"
38
82
  * quando arriva l'evento `user_message` di quel vassallo.
39
83
  */
40
- deliver(kingdomId, slug, text, messageId) {
84
+ deliver(kingdomId, slug, text, messageId, parts, immediate = false) {
41
85
  const body = String(text ?? "").trim();
42
- if (!body)
86
+ const content = parts ?? body;
87
+ if (typeof content === "string" ? !content : !content.some((part) => part.type !== "text" || part.text.trim())) {
43
88
  return { ok: false, error: "messaggio vuoto" };
89
+ }
44
90
  const live = this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()));
45
- if (!live)
91
+ if (!live || live.stopping)
92
+ return { ok: false, error: "collaboratore non piu' attivo" };
93
+ try {
94
+ const id = live.inbox.push(content, messageId);
95
+ if (immediate)
96
+ live.inbox.promote(id);
97
+ live.wake?.();
98
+ return { ok: true, messageId: id };
99
+ }
100
+ catch (err) {
101
+ return { ok: false, error: err.message };
102
+ }
103
+ }
104
+ /** Accoda la compattazione reale; runTurn la esegue al prossimo confine sicuro. */
105
+ compact(kingdomId, slug, instructions) {
106
+ const live = this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()));
107
+ if (!live || live.stopping)
46
108
  return { ok: false, error: "collaboratore non piu' attivo" };
109
+ if (!live.inbox.requestCompact)
110
+ return { ok: false, error: "compattazione non disponibile per questo collaboratore" };
47
111
  try {
48
- return { ok: true, messageId: live.inbox.push(body, messageId) };
112
+ live.inbox.requestCompact(instructions);
113
+ live.wake?.();
114
+ return { ok: true };
49
115
  }
50
116
  catch (err) {
51
117
  return { ok: false, error: err.message };
52
118
  }
53
119
  }
120
+ /** Richiede l'arresto del solo worker. Il finally del runtime rilascia il registro. */
121
+ stop(kingdomId, slug) {
122
+ const live = this.vassals.get(key(String(kingdomId || "").trim(), String(slug || "").trim()));
123
+ if (!live || live.stopping)
124
+ return { ok: false, error: "collaboratore non piu' attivo" };
125
+ if (!live.active)
126
+ return { ok: false, error: "collaboratore gia' inattivo" };
127
+ if (!live.stop)
128
+ return { ok: false, error: "arresto non disponibile per questo collaboratore" };
129
+ live.stopping = true;
130
+ try {
131
+ live.stop();
132
+ return { ok: true };
133
+ }
134
+ catch (err) {
135
+ live.stopping = false;
136
+ return { ok: false, error: err.message };
137
+ }
138
+ }
54
139
  clear(kingdomId) {
55
140
  const id = String(kingdomId || "").trim();
56
- for (const [k, v] of [...this.vassals])
141
+ for (const v of [...this.vassals.values()])
57
142
  if (v.kingdomId === id)
58
- this.vassals.delete(k);
143
+ this.release(v.kingdomId, v.slug);
144
+ }
145
+ clearAll() {
146
+ for (const v of [...this.vassals.values()])
147
+ this.release(v.kingdomId, v.slug);
59
148
  }
60
149
  }
61
150
  export const kingdomLive = new KingdomLive();
151
+ /** Casella stabile del loop, anche quando l'host collega la sua coda interattiva. */
152
+ export function createWorkerInbox() {
153
+ let taken = [];
154
+ let current = createUserInbox();
155
+ const controls = createUserInbox();
156
+ const subscriptions = new Map();
157
+ const inbox = {
158
+ push: (input, id) => current.push(input, id),
159
+ promote: id => current.promote(id),
160
+ get pending() { return taken.length + current.pending; },
161
+ get queued() { return taken.length + (current.queued ?? current.pending); },
162
+ drain: () => { const pending = [...taken, ...current.drain()]; taken = []; return pending; },
163
+ onImmediate(listener) {
164
+ subscriptions.set(listener, current.onImmediate?.(listener) ?? (() => { }));
165
+ return () => { subscriptions.get(listener)?.(); subscriptions.delete(listener); };
166
+ },
167
+ requestCompact: instructions => controls.requestCompact(instructions),
168
+ drainCompactions: () => controls.drainCompactions(),
169
+ get pendingCompactions() { return controls.pendingCompactions; },
170
+ };
171
+ return { inbox,
172
+ take() { taken.push(...current.drain()); return taken.shift(); },
173
+ bind(next) {
174
+ if (next === current || next === inbox)
175
+ return;
176
+ const pending = current.drain();
177
+ for (const off of subscriptions.values())
178
+ off();
179
+ current = next;
180
+ for (const message of pending) {
181
+ current.push(message.parts, message.id);
182
+ if (message.immediate)
183
+ current.promote(message.id);
184
+ }
185
+ for (const listener of subscriptions.keys())
186
+ subscriptions.set(listener, current.onImmediate?.(listener) ?? (() => { }));
187
+ },
188
+ dispose() {
189
+ for (const off of subscriptions.values())
190
+ off();
191
+ subscriptions.clear();
192
+ taken = [];
193
+ current = createUserInbox();
194
+ controls.drainCompactions?.();
195
+ },
196
+ };
197
+ }
@@ -1,8 +1,12 @@
1
- import type { EventSink, ToolContext } from "../core/types.js";
1
+ import type { AgentEvent, EventSink, ToolContext } from "../core/types.js";
2
2
  import type { Harness } from "../core/bootstrap.js";
3
3
  import { type CastleSpec, type VassalReport } from "./kingdom.js";
4
4
  import type { FileCoordinationContext } from "../core/file-coordination.js";
5
5
  export interface KingdomRunInput {
6
+ readonly kingdomId?: string;
7
+ readonly resumeSessions?: Readonly<Record<string, string>>;
8
+ readonly retainSessions?: boolean | "queued";
9
+ readonly onWorkerEvent?: (slug: string, event: AgentEvent) => void;
6
10
  readonly harness: Harness;
7
11
  readonly castles: readonly CastleSpec[];
8
12
  readonly signal: AbortSignal;
@@ -35,7 +35,7 @@ export async function runKingdomForHost(input) {
35
35
  // La sessione dura quanto la conversazione, uno swarm no. Un id riusato
36
36
  // faceva fondere nella UI il regno concluso con quello successivo.
37
37
  kingdomRunSequence = (kingdomRunSequence + 1) % 0xfffff;
38
- const kingdomId = `kw-${harness.session.id}-${Date.now().toString(36)}-${kingdomRunSequence.toString(36)}`;
38
+ const kingdomId = input.kingdomId ?? `kw-${harness.session.id}-${Date.now().toString(36)}-${kingdomRunSequence.toString(36)}`;
39
39
  const concurrency = warmConcurrency(env);
40
40
  const worktree = worktreeMode(env);
41
41
  // Bus pulito per il nuovo regno (in memoria e sul canale persistente).
@@ -89,6 +89,7 @@ export async function runKingdomForHost(input) {
89
89
  sink({ type: "kingdom", phase: "say", slug, text: buffered.slice(0, SAY_MAX), at: Date.now() });
90
90
  };
91
91
  const onEvent = (slug, event) => {
92
+ input.onWorkerEvent?.(slug, event);
92
93
  if (event.type === "usage") {
93
94
  sink({ ...event, record: { ...event.record, actor: event.record.actor ?? slug } });
94
95
  return;
@@ -312,6 +313,8 @@ export async function runKingdomForHost(input) {
312
313
  signal,
313
314
  confirm,
314
315
  ask: input.ask,
316
+ retainSessions: input.retainSessions,
317
+ resumeSessions: input.resumeSessions,
315
318
  concurrency,
316
319
  onEvent,
317
320
  onMemberState: (slug, status, report) => {
@@ -364,7 +367,8 @@ export async function runKingdomForHost(input) {
364
367
  // Ultime parole rimaste nel buffer di chi ha finito senza un confine di turno.
365
368
  for (const slug of [...saying.keys()])
366
369
  flushSay(slug);
367
- kingdomLive.clear(kingdomId);
370
+ if (!input.retainSessions)
371
+ kingdomLive.clear(kingdomId);
368
372
  for (const report of reports) {
369
373
  updateTeamMember(kingdomId, report.slug, { status: report.ok ? "completed" : "failed", summary: report.summary, error: report.error, artifacts: report.artifacts });
370
374
  sink({
@@ -45,6 +45,9 @@ export interface ConflictEntry {
45
45
  export declare function collectConflicts(reports: readonly VassalReport[]): ConflictEntry[];
46
46
  export declare function formatConflictList(conflicts: readonly ConflictEntry[], base?: string): string;
47
47
  export interface KingdomWarmOptions {
48
+ /** Explicit persisted worker sessions for hosts that resume a named worker. */
49
+ readonly resumeSessions?: Readonly<Record<string, string>>;
50
+ readonly retainSessions?: boolean | "queued";
48
51
  readonly harness: Harness;
49
52
  readonly kingdomId: string;
50
53
  readonly castles: readonly CastleSpec[];
@@ -4,8 +4,8 @@ import { existsSync } from "node:fs";
4
4
  import { resolve, sep } from "node:path";
5
5
  import { runTurn } from "../core/agent.js";
6
6
  import { Session } from "../core/session.js";
7
- import { createUserInbox } from "../core/user-inbox.js";
8
- import { kingdomLive } from "./kingdom-live.js";
7
+ import { createWorkerInbox } from "./kingdom-live.js";
8
+ import { createWorkerSession } from "./worker-session.js";
9
9
  import { fileActor, withFileActor } from "../core/file-coordination.js";
10
10
  import { RoomMemory } from "./room-memory.js";
11
11
  import { VascendPlanStore } from "./plan-store.js";
@@ -661,6 +661,7 @@ function subTeamSpawner(opts, castle, mandate, iso, planStoreId) {
661
661
  const report = await launchGranularKingdom({
662
662
  harness: opts.harness,
663
663
  kingdomId: opts.kingdomId,
664
+ retainSessions: opts.retainSessions,
664
665
  castleSlug: castle.slug,
665
666
  units,
666
667
  signal: opts.signal,
@@ -691,10 +692,14 @@ function subTeamSpawner(opts, castle, mandate, iso, planStoreId) {
691
692
  };
692
693
  }
693
694
  async function runVassal(opts, provider, modelId, displayModel, castle, peers, iso, prerequisites = []) {
694
- const { harness, kingdomId, signal, confirm, onEvent } = opts;
695
+ const { harness, kingdomId, signal: parentSignal, confirm, onEvent } = opts;
696
+ let controller = new AbortController();
697
+ const signal = controller.signal;
695
698
  // Sessione (conversazione) dedicata al vassallo; store memoria CONDIVISO del
696
699
  // regno (kingdomId) con id stanze namespaced sullo slug del castello.
697
- const session = Session.create(harness.config.sessionDir, `${kingdomId}-${castle.slug}`);
700
+ const resumeId = opts.resumeSessions?.[castle.slug];
701
+ const session = resumeId ? Session.load(harness.config.sessionDir, resumeId)
702
+ : Session.create(harness.config.sessionDir, `${kingdomId}-${castle.slug}`);
698
703
  const effort = ["low", "medium", "high", "max"].includes(String(castle.effort ?? "").toLowerCase())
699
704
  ? String(castle.effort).toLowerCase()
700
705
  : "";
@@ -719,8 +724,8 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
719
724
  // Casella del vassallo: l'utente puo' scrivergli mentre lavora dalla sua
720
725
  // scheda, e il messaggio entra nel contesto al primo confine sicuro come in
721
726
  // chat principale. Senza, l'unico modo di correggerlo era fermare il regno.
722
- const inbox = createUserInbox();
723
- kingdomLive.register(kingdomId, castle.slug, inbox);
727
+ const binding = createWorkerInbox();
728
+ const inbox = binding.inbox;
724
729
  const workerTools = harness.tools.scoped(castle.tools);
725
730
  for (const name of ["team_say", "team_inbox", "team_wait", "team_report", "vascend_status"])
726
731
  workerTools.promote(name);
@@ -729,6 +734,9 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
729
734
  tools: workerTools,
730
735
  session,
731
736
  config: runConfig,
737
+ env: harness.env,
738
+ fsRoots: harness.fsRoots,
739
+ executionTarget: harness.executionTarget,
732
740
  confirm,
733
741
  ask: opts.ask,
734
742
  host: undefined,
@@ -739,7 +747,8 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
739
747
  // Delega di secondo livello: un castello grande si gestisce formando un team
740
748
  // sulle proprie stanze. Prima questo campo mancava, e kingdoms_warm dentro
741
749
  // un vassallo rispondeva "non disponibile": la gerarchia era ferma a 1.
742
- spawnKingdom: subTeamSpawner({ ...opts, harness: { ...harness, config: runConfig, tools: workerTools } }, castle, mandate, iso, planStoreId),
750
+ spawnKingdom: (specs) => subTeamSpawner({ ...opts, signal: controller.signal,
751
+ harness: { ...harness, config: runConfig, tools: workerTools } }, castle, mandate, iso, planStoreId)(specs),
743
752
  // isolated abilita il git nel vassallo (worktree proprio); altrimenti il
744
753
  // bash tool blocca il git che muta (working dir condivisa).
745
754
  kingdom: { id: kingdomId, self: castle.slug, isolated: iso.isolated, peers },
@@ -772,8 +781,13 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
772
781
  completed = true;
773
782
  onEvent?.(castle.slug, event);
774
783
  };
784
+ const worker = createWorkerSession({ kingdomId, slug: castle.slug, sessionId: session.id, goal: castle.goal,
785
+ parentSignal, controller, binding, deps, sink, retain: !iso.isolated ? opts.retainSessions ?? false : false,
786
+ onController: next => { controller = next; } });
775
787
  try {
776
788
  const role = castle.role?.trim() || "collaboratore";
789
+ sink({ type: "kingdom", phase: "session", kingdomId, sessionId: session.id,
790
+ slug: castle.slug, state: "spawned", goal: castle.goal, at: Date.now() });
777
791
  const parent = castle.parent?.trim() || "orchestratore";
778
792
  const instructions = (castle.area?.length ? `\nAREA_ASSEGNATA: ${castle.area.join(", ")}. Coordina modifiche fuori area prima di eseguirle.\n` : "") + (castle.instructions?.trim()
779
793
  ? `\nISTRUZIONI_AGGIUNTIVE\n${castle.instructions.trim()}\n\n`
@@ -825,10 +839,7 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
825
839
  };
826
840
  }
827
841
  finally {
828
- deps.fileCoordination?.hub.clearActor(deps.fileCoordination.actor.id);
829
- // Il collaboratore non c'e' piu': la UI deve saperlo subito, altrimenti
830
- // continuerebbe a offrire un campo di scrittura che non consegna nulla.
831
- kingdomLive.release(kingdomId, castle.slug);
842
+ worker.finish();
832
843
  }
833
844
  }
834
845
  function mandateCompletion(sessionDir, planStoreId, mandate) {
@@ -1,3 +1,4 @@
1
+ import { ContextStoreReader } from "./context-store-reader.js";
1
2
  export interface MemoryPacketInput {
2
3
  readonly sessionDir: string;
3
4
  readonly scopeId: string;
@@ -9,5 +10,5 @@ export interface MemoryPacketInput {
9
10
  /** Preselected dependencies supplied by a coherent snapshot. */
10
11
  readonly projectScopes?: readonly string[];
11
12
  }
12
- export declare function buildMemoryPacket(input: MemoryPacketInput): string;
13
+ export declare function buildMemoryPacket(input: MemoryPacketInput, reader?: ContextStoreReader): string;
13
14
  export declare function clampMemoryPacket(value: string, max: number): string;