@schlenkr/ragents 0.1.14 → 0.1.16

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 (114) hide show
  1. package/apps/server/package.json +1 -1
  2. package/apps/server/src/api/contracts.ts +19 -1
  3. package/apps/server/src/api/core-methods.ts +12 -0
  4. package/apps/server/src/chat-handler.ts +19 -0
  5. package/apps/server/src/host-api.json +2 -0
  6. package/apps/server/src/host-api.ts +2 -2
  7. package/apps/server/src/plugin-build/build.ts +1 -1
  8. package/apps/server/src/plugin-support/actor-programs/app-project.ts +62 -18
  9. package/apps/server/src/plugin-support/actor-programs/client-contracts.ts +47 -8
  10. package/apps/server/src/plugin-support/actor-programs/contract.ts +18 -1
  11. package/apps/server/src/plugin-support/actor-programs/service.ts +22 -1
  12. package/apps/server/src/plugin-support/plugin-folder.ts +14 -1
  13. package/apps/server/src/plugin-support/run-scripts.ts +27 -2
  14. package/apps/server/src/profile/compose.ts +2 -0
  15. package/apps/server/src/provider.ts +23 -4
  16. package/apps/server/src/ragents/chat-projection.ts +7 -0
  17. package/apps/server/src/ragents/global-chat.ts +5 -1
  18. package/apps/server/src/ragents/run-script-tools.ts +66 -0
  19. package/apps/server/src/ragents/session.ts +122 -60
  20. package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-BslIwn_y.js → highlighted-body-KPVGNVTW-BwD4uYEe.js} +1 -1
  21. package/apps/web/dist/assets/{main-BUN8m8SW.js → main-KBrc5Q77.js} +1 -1
  22. package/apps/web/dist/assets/{mermaid-HWGCJPDP-B1erZlFM.js → mermaid-HWGCJPDP-CSCkaArr.js} +88 -88
  23. package/apps/web/dist/assets/runPanel-BjG5NBKm.js +26 -0
  24. package/apps/web/dist/help/guide-clients.html +21 -4
  25. package/apps/web/dist/help/guide-clients.md +23 -4
  26. package/apps/web/dist/help/guide-plugins.html +12 -2
  27. package/apps/web/dist/help/guide-plugins.md +9 -2
  28. package/apps/web/dist/help/guide-programs.html +8 -2
  29. package/apps/web/dist/help/guide-programs.md +9 -2
  30. package/apps/web/dist/help/guide-runtime.html +3 -2
  31. package/apps/web/dist/help/guide-runtime.md +3 -2
  32. package/apps/web/dist/help/index.html +1 -1
  33. package/apps/web/dist/help/mini-app.css +7 -2
  34. package/apps/web/dist/host-web.json +6 -6
  35. package/apps/web/dist/index.html +2 -2
  36. package/apps/web/dist/run-panel.html +2 -2
  37. package/apps/web/package.json +1 -1
  38. package/apps/web/src/run-panel/RunPanelApp.tsx +1 -1
  39. package/apps/web/src/run-panel/RunPanelMenu.tsx +47 -11
  40. package/bundles/ragents.activity/ragents-bundle.json +1 -1
  41. package/bundles/ragents.actor-programs/ragents-bundle.json +4 -2
  42. package/bundles/ragents.actor-programs/server/index.js +437 -24
  43. package/bundles/ragents.actor-programs/server/index.js.map +4 -4
  44. package/bundles/ragents.actor-programs/web/index.js.map +1 -1
  45. package/bundles/ragents.ask/ragents-bundle.json +2 -2
  46. package/bundles/ragents.ask/server/index.js +1 -0
  47. package/bundles/ragents.ask/server/index.js.map +2 -2
  48. package/bundles/ragents.browser/ragents-bundle.json +1 -1
  49. package/bundles/ragents.documents/ragents-bundle.json +1 -1
  50. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
  51. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
  52. package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
  53. package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
  54. package/bundles/ragents.orchestration/ragents-bundle.json +7 -4
  55. package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js +11 -0
  56. package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js.map +7 -0
  57. package/bundles/ragents.orchestration/server/exports/server/contract.js +9 -0
  58. package/bundles/ragents.orchestration/server/exports/server/contract.js.map +7 -0
  59. package/bundles/ragents.orchestration/server/index.js +49 -3
  60. package/bundles/ragents.orchestration/server/index.js.map +2 -2
  61. package/bundles/ragents.orchestration/surface.hbs +5 -0
  62. package/bundles/ragents.overseer/ragents-bundle.json +1 -1
  63. package/bundles/ragents.processes/ragents-bundle.json +1 -1
  64. package/bundles/ragents.product/ragents-bundle.json +1 -1
  65. package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
  66. package/bundles/ragents.reference/actors/notebook/package.json +16 -0
  67. package/bundles/ragents.reference/actors/notebook/src/client.tsx +31 -0
  68. package/bundles/ragents.reference/actors/notebook/src/server.ts +27 -0
  69. package/bundles/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
  70. package/bundles/ragents.reference/ragents-bundle.json +4 -3
  71. package/bundles/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
  72. package/bundles/ragents.reference/run-scripts/quick-note/package.json +9 -0
  73. package/bundles/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
  74. package/bundles/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
  75. package/bundles/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
  76. package/bundles/ragents.reference/run-scripts/run-roster/package.json +9 -0
  77. package/bundles/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
  78. package/bundles/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
  79. package/bundles/ragents.todo/ragents-bundle.json +1 -1
  80. package/bundles/ragents.transcript/ragents-bundle.json +1 -1
  81. package/bundles/ragents.watch/ragents-bundle.json +1 -1
  82. package/bundles/ragents.workspace/ragents-bundle.json +1 -1
  83. package/package.json +3 -3
  84. package/packages/ragents/src/domain/vocabulary.ts +4 -0
  85. package/packages/ragents/src/index.ts +3 -1
  86. package/packages/ragents/src/plugin-host.ts +62 -10
  87. package/packages/ragents/src/plugin-types.ts +11 -0
  88. package/packages/ragents/src/runtime/guards.ts +2 -1
  89. package/packages/ragents/src/typescript/async-compiler.ts +11 -1
  90. package/plugins/ragents.actor-programs/server/index.ts +3 -0
  91. package/plugins/ragents.actor-programs/server/runtime.ts +424 -28
  92. package/plugins/ragents.actor-programs/server/script-state.ts +54 -0
  93. package/plugins/ragents.actor-programs/server/tool-contributor.ts +2 -0
  94. package/plugins/ragents.ask/server/ask-tool.ts +1 -0
  95. package/plugins/ragents.orchestration/ragents-plugin.json +1 -0
  96. package/plugins/ragents.orchestration/server/contract.ts +14 -0
  97. package/plugins/ragents.orchestration/server/index.ts +4 -1
  98. package/plugins/ragents.orchestration/server/surface-tool.ts +58 -2
  99. package/plugins/ragents.orchestration/surface.hbs +5 -0
  100. package/plugins/ragents.reference/actors/notebook/package.json +16 -0
  101. package/plugins/ragents.reference/actors/notebook/src/client.tsx +31 -0
  102. package/plugins/ragents.reference/actors/notebook/src/server.ts +27 -0
  103. package/plugins/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
  104. package/plugins/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
  105. package/plugins/ragents.reference/run-scripts/quick-note/package.json +9 -0
  106. package/plugins/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
  107. package/plugins/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
  108. package/plugins/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
  109. package/plugins/ragents.reference/run-scripts/run-roster/package.json +9 -0
  110. package/plugins/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
  111. package/plugins/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
  112. package/scripts/agent/agent-cli.ts +46 -2
  113. package/scripts/package/ragents.mjs +3 -1
  114. package/apps/web/dist/assets/runPanel-s8gGpr9M.js +0 -21
@@ -2,16 +2,18 @@ import { randomUUID } from "node:crypto";
2
2
  import { lstat, mkdir, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
3
3
  import { lstatSync, readFileSync, renameSync } from "node:fs";
4
4
  import path from "node:path";
5
+ import { isDeepStrictEqual } from "node:util";
5
6
  import { Type, type TSchema } from "typebox";
6
7
  import { Value } from "typebox/value";
7
- import { actorByHandle, actorDescriptionMaxLength, agentTools, assertJsonValue, canonicalHash, defineRunFunction, defineToolAvailability, emptyUsage, handleKey, runCapabilityContractHash, scriptInputOf, schemaComplaints, type Actor, type ActorProgramExecutor, type RunFunction, type CommandContext, type ExecutableActor, type JsonValue, type Orchestration, type PluginContext, type RunCapabilityDescriptor, type RunView, type TurnRequest, type TurnResult, } from "@ragents/engine";
8
- import { ACTOR_PROGRAMS_STATE_ID, ACTOR_INVOCATIONS_STATE_ID, ACTOR_SCRIPT_STATE_ID, ACTOR_STATE_ID, resolveActorView, type ActorDataState, type ActorFunctionDefinition, type ActorFunctionInvocation, type ActorProgramDefinition, type ActorProgramState, type ActorScriptState, type ActorViewListing, } from "@ragents/host/plugin-support/actor-programs/contract.js";
9
- import type { ActorProgramSource, ActorProgramsService } from "@ragents/host/plugin-support/actor-programs/service.js";
8
+ import { actorByHandle, actorDescriptionMaxLength, agentTools, assertJsonValue, canonicalHash, defineRunFunction, defineToolAvailability, emptyUsage, handleKey, inheritedGrants, runCapabilityContractHash, scriptInputOf, schemaComplaints, type Actor, type ActorProgramExecutor, type RunFunction, type CommandContext, type ExecutableActor, type JsonValue, type Orchestration, type PluginContext, type RunCapabilityDescriptor, type RunView, type TurnRequest, type TurnResult, } from "@ragents/engine";
9
+ import { ACTOR_PROGRAMS_STATE_ID, ACTOR_INVOCATIONS_STATE_ID, ACTOR_SCRIPT_STATE_ID, ACTOR_STATE_ID, resolveActorView, type ActorDataState, type ActorFunctionDefinition, type ActorFunctionInvocation, type ActorPackageOrigin, type ActorProgramDefinition, type ActorProgramIdentity, type ActorProgramState, type ActorScriptDelivery, type ActorScriptPackage, type ActorScriptState, type ActorViewListing, } from "@ragents/host/plugin-support/actor-programs/contract.js";
10
+ import type { ActivatedActorProgram, ActorProgramSource, ActorProgramsService, RunScriptSources, RunScriptStartInput } from "@ragents/host/plugin-support/actor-programs/service.js";
10
11
  import { jsonValue, jsonBytes, MAX_INVOCATIONS_STATE_BYTES } from "./limits.js";
12
+ import { agentResultText, finishesOf, MAX_OPEN_STARTS, scriptResultContent, scriptStateOf, type ScriptFinish } from "./script-state.js";
11
13
  import { agentCapabilityBinding, operatorCapabilityBinding } from "./capability-resolver.js";
12
14
  import type { ActorOperationPort } from "./operations.js";
13
15
  import { compileClientProject, installClientSdk } from "@ragents/host/plugin-support/actor-programs/client-compiler.js";
14
- import { compileAppBackend, installServerSdk, prepareAppProject, prepareAppWorkspace, projectSourceFiles, readAppPackage, typecheckServerProject, type AppContract } from "@ragents/host/plugin-support/actor-programs/app-project.js";
16
+ import { compileAppBackend, installServerSdk, packageIdentity, prepareAppProject, prepareAppWorkspace, projectSourceFiles, readAppPackage, typecheckServerProject, type AppContract } from "@ragents/host/plugin-support/actor-programs/app-project.js";
15
17
  import { syncWorkspaceOwnership } from "@ragents/host/plugin-support/workspace-ownership.js";
16
18
  import { runManagedProcess, sandboxedLaunch, type WorkspaceProcessContext } from "@ragents/workspace-executor";
17
19
  import { runModuleTemplates, templateById, templateFiles } from "./templates.js";
@@ -88,6 +90,21 @@ const commitPackage = (staged: string, directory: string, name: string): void =>
88
90
  throw error;
89
91
  }
90
92
  };
93
+ const sameSources = async (directory: string, files: readonly ActorProgramSource[]): Promise<boolean> =>
94
+ packageIdentity(await projectSourceFiles(directory)) === packageIdentity(files);
95
+ const sameOrigin = (left: ActorPackageOrigin, right: ActorPackageOrigin): boolean =>
96
+ left.kind === "script" ? right.kind === "script" && left.entryId === right.entryId : right.kind === "shared" && left.pluginId === right.pluginId;
97
+ const originText = (origin: ActorPackageOrigin | undefined): string =>
98
+ !origin ? "that no run script installed" : origin.kind === "script" ? `from the run script ${origin.entryId}` : `shared by ${origin.pluginId}`;
99
+ /** A package the host installs for a run script, with the origin it records. */
100
+ interface OriginPackage { name: string; files: readonly ActorProgramSource[]; origin: ActorPackageOrigin }
101
+ /** What a claim wrote for a name and what it replaced; a release restores only a record nobody changed since. */
102
+ interface Claim { name: string; claimed: ActorScriptPackage; previous: ActorScriptPackage | undefined }
103
+ /** A name reserved by a shared package that the run holds for something else. */
104
+ const foreignShared = (name: string, pluginId: string, record: ActorScriptPackage | undefined): Error => {
105
+ const holder = !record ? "that the host did not install" : sameOrigin(record.origin, { kind: "shared", pluginId }) ? "that was changed in the run" : originText(record.origin);
106
+ return new Error(`${name} is the shared actor package of ${pluginId}, but this run has a package ${name} ${holder}; the host neither uses nor replaces it. Remove or rename that package.`);
107
+ };
91
108
  const descriptor = (tool: RunFunction): RunCapabilityDescriptor => ({ id: tool.name, label: tool.label, description: tool.description, schema: tool.schema, resultSchema: tool.resultSchema });
92
109
  interface BackendBinding {
93
110
  id: string;
@@ -131,7 +148,12 @@ export interface ActorProgramRuntimeOptions {
131
148
  operations: ActorOperationPort;
132
149
  directoryFor: (runId: string) => string;
133
150
  scriptSources: (entryId: string, name: string) => readonly ActorProgramSource[] | undefined;
151
+ /** A shared actor package of the profile, by name. */
152
+ sharedPackage: (name: string) => { pluginId: string; files: readonly ActorProgramSource[] } | undefined;
153
+ /** Places a surface entity next to the run's layout unless the layout already shows it. */
154
+ placeView: (context: CommandContext, runId: string, entity: string) => void;
134
155
  }
156
+ type Recognized = { start: JsonValue } | { result: JsonValue };
135
157
  export class ActorProgramRuntime implements ActorProgramsService, ActorProgramExecutor {
136
158
  readonly #options: ActorProgramRuntimeOptions;
137
159
  readonly #queues = new Map<string, Promise<void>>();
@@ -146,6 +168,9 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
146
168
  readonly #staged = new Set<string>();
147
169
  readonly #removing = new Set<string>();
148
170
  readonly #knownRuns = new Set<string>();
171
+ readonly #recovered = new Set<string>();
172
+ readonly #packageLocks = new Map<string, Promise<void>>();
173
+ readonly #identities = new Map<string, string>();
149
174
  readonly #epochs = new Map<string, number>();
150
175
  readonly #stopping = new Set<string>();
151
176
  #shuttingDown = false;
@@ -189,8 +214,42 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
189
214
  #writeProgram(context: CommandContext, runId: string, actorId: string, program: ActorProgramDefinition | null): void {
190
215
  this.runtime().replacePluginState(this.operatorContext(runId), runId, { pluginId: ACTOR_PROGRAMS_STATE_ID, scope: { kind: "actor", actorId }, state: jsonValue({ version: 1, program }, "Actor program") });
191
216
  }
192
- #script(runId: string): ActorScriptState | undefined {
193
- return this.view(runId).pluginStates.find((entry) => entry.pluginId === ACTOR_SCRIPT_STATE_ID && entry.scope.kind === "run")?.state as unknown as ActorScriptState | undefined;
217
+ /** Which run script installed which package, and the inputs the host queued for programs to recognize. */
218
+ #script(runId: string): ActorScriptState {
219
+ return scriptStateOf(this.view(runId).pluginStates.find((entry) => entry.pluginId === ACTOR_SCRIPT_STATE_ID && entry.scope.kind === "run")?.state);
220
+ }
221
+ /** Keeps only deliveries whose input is still queued or being processed, and `keep`, whose input follows right after this write. */
222
+ #writeScript(runId: string, state: ActorScriptState, keep?: string): void {
223
+ const live = this.#liveCommands(runId, new Set(state.deliveries.map((delivery) => delivery.actorId)));
224
+ const deliveries = state.deliveries.filter((delivery) => delivery.commandId === keep || live.has(delivery.commandId));
225
+ this.runtime().replacePluginState(this.operatorContext(runId, "script"), runId, { pluginId: ACTOR_SCRIPT_STATE_ID, scope: { kind: "run" }, state: jsonValue({ ...state, deliveries }, "Run script state") });
226
+ }
227
+ /** The commands that queued inputs of these actors which are pending or belong to a running turn. */
228
+ #liveCommands(runId: string, actorIds: ReadonlySet<string>): Set<string> {
229
+ const view = this.view(runId);
230
+ const running = new Set(view.turns.filter((turn) => turn.status === "running").map((turn) => turn.id));
231
+ const sequences = new Set(view.inputs.filter((input) => actorIds.has(input.actorId)
232
+ && (input.lifecycle.kind === "pending" || input.lifecycle.kind === "claimed" && running.has(input.lifecycle.turnId))).map((input) => input.sequence));
233
+ const commands = new Set<string>();
234
+ if (sequences.size === 0)
235
+ return commands;
236
+ const oldest = Math.min(...sequences);
237
+ for (const event of this.runtime().recentEvents(runId)) {
238
+ if (event.sequence < oldest)
239
+ break;
240
+ if (sequences.has(event.sequence))
241
+ commands.add(event.commandId);
242
+ }
243
+ return commands;
244
+ }
245
+ #commandOf(runId: string, sequence: number): string | undefined {
246
+ for (const event of this.runtime().recentEvents(runId)) {
247
+ if (event.sequence === sequence)
248
+ return event.commandId;
249
+ if (event.sequence < sequence)
250
+ return undefined;
251
+ }
252
+ return undefined;
194
253
  }
195
254
  async #writeSources(directory: string, files: readonly ActorProgramSource[]): Promise<void> {
196
255
  for (const source of files) {
@@ -201,10 +260,31 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
201
260
  await writeFile(file, source.content, { flag: "wx" });
202
261
  }
203
262
  }
204
- async #refresh(runId: string, program: ActorProgramDefinition, signal: AbortSignal): Promise<void> {
205
- const script = this.#script(runId);
206
- const files = script ? this.#options.scriptSources(script.entryId, program.name) : undefined;
207
- if (files) {
263
+ #originSources(name: string, origin: ActorPackageOrigin): readonly ActorProgramSource[] | undefined {
264
+ if (origin.kind === "script")
265
+ return this.#options.scriptSources(origin.entryId, name);
266
+ const shared = this.#options.sharedPackage(name);
267
+ return shared?.pluginId === origin.pluginId ? shared.files : undefined;
268
+ }
269
+ /** The identity of the sources the program was built from; a build never changes, so it is read once. */
270
+ #identityOf(program: ActorProgramDefinition): string {
271
+ const known = this.#identities.get(program.directory);
272
+ if (known !== undefined)
273
+ return known;
274
+ const identity = packageIdentity(JSON.parse(readFileSync(path.join(program.directory, "sources.json"), "utf8")) as ActorProgramSource[]);
275
+ this.#identities.set(program.directory, identity);
276
+ return identity;
277
+ }
278
+ /** A package still built from what its origin installed reloads from the origin's current sources; one changed in the run keeps its own. */
279
+ #refresh(runId: string, program: ActorProgramDefinition, signal: AbortSignal): Promise<void> {
280
+ return this.#withPackages(runId, [program.name], () => this.#reload(runId, program, signal));
281
+ }
282
+ async #reload(runId: string, program: ActorProgramDefinition, signal: AbortSignal): Promise<void> {
283
+ const record = this.#script(runId).packages[program.name];
284
+ const files = record && this.#identityOf(program) === record.identity ? this.#originSources(program.name, record.origin) : undefined;
285
+ if (record && files) {
286
+ const state = this.#script(runId);
287
+ this.#writeScript(runId, { ...state, packages: { ...state.packages, [program.name]: { ...state.packages[program.name]!, identity: packageIdentity(files) } } });
208
288
  const directory = path.join(await this.workspaceDirectory(runId), program.name);
209
289
  for (const entry of await readdir(directory))
210
290
  if (entry !== "node_modules")
@@ -242,6 +322,31 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
242
322
  throw new Error(`The capability contract of ${program.name} still differs after the reactivation.`);
243
323
  return refreshed;
244
324
  }
325
+ async #acquire(key: string): Promise<() => void> {
326
+ const previous = this.#packageLocks.get(key) ?? Promise.resolve();
327
+ const released = Promise.withResolvers<void>();
328
+ const held = previous.then(() => released.promise);
329
+ this.#packageLocks.set(key, held);
330
+ await previous;
331
+ return () => {
332
+ released.resolve();
333
+ if (this.#packageLocks.get(key) === held)
334
+ this.#packageLocks.delete(key);
335
+ };
336
+ }
337
+ /** One exclusion per run and package name for creating, activating, installing and ensuring; callers wait instead of failing. */
338
+ async #withPackages<T>(runId: string, names: readonly string[], work: () => Promise<T>): Promise<T> {
339
+ const releases: (() => void)[] = [];
340
+ try {
341
+ for (const name of [...new Set(names)].sort())
342
+ releases.push(await this.#acquire(`${runId}\0${name}`));
343
+ return await work();
344
+ }
345
+ finally {
346
+ for (const release of releases.reverse())
347
+ release();
348
+ }
349
+ }
245
350
  async workspaceDirectory(runId: string): Promise<string> { this.#knownRuns.add(runId); return prepareAppWorkspace(path.join(this.#options.directoryFor(runId), "actor-workspace")); }
246
351
  templates() { return runModuleTemplates.map(({ id, title, description }) => ({ id, title, description })); }
247
352
  async #create<T>(runId: string, name: string, build: (directory: string) => Promise<T>): Promise<{ directory: string; built: T }> {
@@ -273,7 +378,10 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
273
378
  await rm(staged, { recursive: true, force: true });
274
379
  }
275
380
  }
276
- async scaffold(runId: string, name: string, templateId: string) {
381
+ scaffold(runId: string, name: string, templateId: string) {
382
+ return this.#withPackages(runId, [name], () => this.#scaffold(runId, name, templateId));
383
+ }
384
+ async #scaffold(runId: string, name: string, templateId: string) {
277
385
  const sources = Object.entries(templateFiles(templateById(templateId), name)).map(([file, content]) => ({ path: file, content }));
278
386
  const { built: files } = await this.#create(runId, name, async (directory) => {
279
387
  await this.#writeSources(directory, sources);
@@ -285,20 +393,288 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
285
393
  });
286
394
  return { name, directory: `@actors/${name}`, files };
287
395
  }
288
- async importPackage(context: CommandContext, runId: string, name: string, files: readonly ActorProgramSource[], signal?: AbortSignal, scriptEntryId?: string) {
396
+ importPackage(context: CommandContext, runId: string, name: string, files: readonly ActorProgramSource[], signal?: AbortSignal): Promise<ActivatedActorProgram> {
397
+ return this.#withPackages(runId, [name], () => this.#import(context, runId, name, files, signal));
398
+ }
399
+ async #import(context: CommandContext, runId: string, name: string, files: readonly ActorProgramSource[], signal?: AbortSignal): Promise<ActivatedActorProgram> {
289
400
  const { directory } = await this.#create(runId, name, (staged) => this.#writeSources(staged, files));
290
401
  try {
291
- if (scriptEntryId)
292
- this.runtime().replacePluginState(this.operatorContext(runId), runId, { pluginId: ACTOR_SCRIPT_STATE_ID, scope: { kind: "run" }, state: { version: 1, entryId: scriptEntryId } });
293
- await this.activate(context, runId, name, signal);
294
- const program = this.programs(runId).find((entry) => entry.name === name)!;
295
- return { name, actorId: program.actorId, actorHandle: program.actorHandle, views: program.views.length };
402
+ await this.#activate(context, runId, name, true, signal);
403
+ return this.#activated(runId, name);
296
404
  }
297
405
  catch (error) {
298
406
  await rm(directory, { recursive: true, force: true });
299
407
  throw error;
300
408
  }
301
409
  }
410
+ #activated(runId: string, name: string): ActivatedActorProgram {
411
+ const program = this.#program(runId, name);
412
+ return { name, actorId: program.actorId, actorHandle: program.actorHandle, views: program.views.length };
413
+ }
414
+ installScript(context: CommandContext, runId: string, script: RunScriptSources, signal?: AbortSignal): Promise<ActivatedActorProgram> {
415
+ return this.#withPackages(runId, [script.handle, ...script.programs.map((program) => program.name), ...script.sharedPrograms], () => this.#install(context, runId, script, signal));
416
+ }
417
+ async #install(context: CommandContext, runId: string, script: RunScriptSources, signal?: AbortSignal): Promise<ActivatedActorProgram> {
418
+ const root = await this.workspaceDirectory(runId);
419
+ const before = this.#script(runId).packages;
420
+ const own: ActorPackageOrigin = { kind: "script", entryId: script.entryId };
421
+ const programs: OriginPackage[] = [
422
+ ...script.programs.map((program) => ({ ...program, origin: own })),
423
+ ...script.sharedPrograms.map((name) => {
424
+ const shared = this.#options.sharedPackage(name);
425
+ if (!shared)
426
+ throw new Error(`The run script ${script.entryId} needs the shared actor package ${name}, which no plugin of this profile provides.`);
427
+ return { name, files: shared.files, origin: { kind: "shared", pluginId: shared.pluginId } as const };
428
+ }),
429
+ ];
430
+ const present = await this.#presentPrograms(root, script.entryId, programs, before);
431
+ const reuse = this.#reusesScript(runId, root, script, before);
432
+ const missing = programs.filter((program) => !present.has(program.name));
433
+ const claims = this.#claim(runId, [...missing, ...(reuse ? [] : [{ name: script.handle, files: script.files, origin: own }])]);
434
+ const copied: string[] = [];
435
+ try {
436
+ for (const program of missing) {
437
+ signal?.throwIfAborted();
438
+ await this.#create(runId, program.name, (staged) => this.#writeSources(staged, program.files));
439
+ copied.push(program.name);
440
+ }
441
+ return reuse ? await this.#restartScript(context, runId, script.handle, signal) : await this.#import(context, runId, script.handle, script.files, signal);
442
+ }
443
+ catch (error) {
444
+ for (const name of copied)
445
+ await rm(path.join(root, name), { recursive: true, force: true });
446
+ try {
447
+ this.#release(runId, claims);
448
+ }
449
+ catch (release) {
450
+ throw new Error(`${errorText(error)} The ownership record could not be removed again (${errorText(release)}); it names no package and the next start replaces it.`);
451
+ }
452
+ throw error;
453
+ }
454
+ }
455
+ /** Ownership is recorded before anything is created, so no package the host installs exists without its origin. */
456
+ #claim(runId: string, packages: readonly OriginPackage[]): readonly Claim[] {
457
+ const state = this.#script(runId);
458
+ const claims = packages.map((entry): Claim => {
459
+ const previous = state.packages[entry.name];
460
+ return { name: entry.name, previous, claimed: { origin: entry.origin, identity: packageIdentity(entry.files), count: previous && sameOrigin(previous.origin, entry.origin) ? previous.count : 0, open: [] } };
461
+ });
462
+ if (claims.length > 0)
463
+ this.#writeScript(runId, { ...state, packages: { ...state.packages, ...Object.fromEntries(claims.map((claim) => [claim.name, claim.claimed])) } });
464
+ return claims;
465
+ }
466
+ /** Compare and swap: a record someone changed since the claim stays as it is. */
467
+ #release(runId: string, claims: readonly Claim[]): void {
468
+ const state = this.#script(runId);
469
+ const released = claims.filter((claim) => isDeepStrictEqual(state.packages[claim.name], claim.claimed));
470
+ if (released.length === 0)
471
+ return;
472
+ const names = new Set(released.map((claim) => claim.name));
473
+ const kept = Object.entries(state.packages).filter(([name]) => !names.has(name));
474
+ const restored = released.flatMap((claim) => claim.previous ? [[claim.name, claim.previous] as const] : []);
475
+ this.#writeScript(runId, { ...state, packages: Object.fromEntries([...kept, ...restored]) });
476
+ }
477
+ /** Whether the host installed this shared package from this plugin and nobody changed it since. */
478
+ #hostShared(record: ActorScriptPackage | undefined, pluginId: string, identity: string): boolean {
479
+ return record !== undefined && sameOrigin(record.origin, { kind: "shared", pluginId }) && record.identity === identity;
480
+ }
481
+ /** The names already in the run that the start keeps; a different package, or one under a shared name the host did not install, is an error before anything is copied. */
482
+ async #presentPrograms(root: string, entryId: string, programs: readonly OriginPackage[], packages: Readonly<Record<string, ActorScriptPackage>>): Promise<ReadonlySet<string>> {
483
+ const present = await Promise.all(programs.map(async (program) => {
484
+ const directory = path.join(root, program.name);
485
+ if (!occupied(directory))
486
+ return false;
487
+ if (program.origin.kind === "shared") {
488
+ if (this.#hostShared(packages[program.name], program.origin.pluginId, packageIdentity(await projectSourceFiles(directory))))
489
+ return true;
490
+ throw foreignShared(program.name, program.origin.pluginId, packages[program.name]);
491
+ }
492
+ if (await sameSources(directory, program.files))
493
+ return true;
494
+ throw new Error(`The run script ${entryId} bundles the program ${program.name}, but this run already has a different package ${program.name} ${originText(packages[program.name]?.origin)}.`);
495
+ }));
496
+ return new Set(programs.filter((_, index) => present[index]).map((program) => program.name));
497
+ }
498
+ /** A recorded origin without actor and folder is left over from a failed start, whichever template it names, and counts as not installed. */
499
+ #reusesScript(runId: string, root: string, script: RunScriptSources, packages: Readonly<Record<string, ActorScriptPackage>>): boolean {
500
+ const holder = this.view(runId).actors.find((actor) => actor.handle === script.handle);
501
+ const exists = occupied(path.join(root, script.handle));
502
+ if (!holder && !exists)
503
+ return false;
504
+ const origin = packages[script.handle]?.origin;
505
+ if (origin !== undefined && !sameOrigin(origin, { kind: "script", entryId: script.entryId }))
506
+ throw new Error(`The package ${script.handle} in this run ${origin.kind === "script" ? `comes from the run script ${origin.entryId}` : `is the shared package of ${origin.pluginId}`}, not from the run script ${script.entryId}.`);
507
+ if (origin !== undefined && exists && (!holder || holder.kind === "script"))
508
+ return true;
509
+ throw new Error(holder
510
+ ? `Handle @${script.handle} already belongs to ${holder.displayName} in this run, not to the run script ${script.entryId}.`
511
+ : `The package ${script.handle} already exists in this run and was not installed by the run script ${script.entryId}.`);
512
+ }
513
+ async #restartScript(context: CommandContext, runId: string, handle: string, signal?: AbortSignal): Promise<ActivatedActorProgram> {
514
+ const view = this.view(runId);
515
+ const holder = view.actors.find((actor) => actor.handle === handle);
516
+ const state = holder && view.pluginStates.find((entry) => entry.pluginId === ACTOR_PROGRAMS_STATE_ID && entry.scope.kind === "actor" && entry.scope.actorId === holder.id)?.state as unknown as ActorProgramState | undefined;
517
+ if (!holder || !state?.program)
518
+ await this.#activate(context, runId, handle, true, signal);
519
+ else if (holder.kind !== "human" && holder.lifecycle.kind === "stopped")
520
+ this.runtime().restartActor(context, runId, holder.id, "Run script started again");
521
+ return this.#activated(runId, handle);
522
+ }
523
+ enqueueStart(context: CommandContext, runId: string, handle: string, start: RunScriptStartInput): { count: number } {
524
+ const program = this.#program(runId, handle);
525
+ const recorded = this.#script(runId).packages[handle];
526
+ if (!recorded)
527
+ throw new Error(`The package ${handle} was not installed by a run script.`);
528
+ const view = program.views[0];
529
+ if (start.embedded && view)
530
+ this.#options.placeView(this.operatorContext(runId, "place"), runId, `app:${view.id}`);
531
+ const state = this.#script(runId);
532
+ const record = state.packages[handle]!;
533
+ const count = record.count + 1;
534
+ const delivery: ActorScriptDelivery = { commandId: context.commandId, actorId: program.actorId, kind: "start", name: handle, count, embedded: start.embedded, startedBy: start.startedBy };
535
+ this.#writeScript(runId, { ...state, packages: { ...state.packages, [handle]: { ...record, count } }, deliveries: [...state.deliveries, delivery] }, context.commandId);
536
+ try {
537
+ this.runtime().enqueueInput(context, runId, { actorId: program.actorId, content: start.content });
538
+ }
539
+ catch (error) {
540
+ const current = this.#script(runId);
541
+ this.#writeScript(runId, { ...current, packages: { ...current.packages, [handle]: { ...current.packages[handle]!, count: record.count } },
542
+ deliveries: current.deliveries.filter((entry) => entry.commandId !== context.commandId) });
543
+ throw error;
544
+ }
545
+ return { count };
546
+ }
547
+ /** A start or result the host queued for this actor, recognized by the command that queued the input; it is consumed here. */
548
+ #recognize(runId: string, actorId: string, input: { sequence: number; content: string }): Recognized | undefined {
549
+ const state = this.#script(runId);
550
+ if (!state.deliveries.some((delivery) => delivery.actorId === actorId))
551
+ return undefined;
552
+ const commandId = this.#commandOf(runId, input.sequence);
553
+ const delivery = state.deliveries.find((entry) => entry.commandId === commandId && entry.actorId === actorId);
554
+ if (!delivery)
555
+ return undefined;
556
+ const deliveries = state.deliveries.filter((entry) => entry !== delivery);
557
+ if (delivery.kind === "result") {
558
+ this.#writeScript(runId, { ...state, deliveries });
559
+ return { result: JSON.parse(input.content) as JsonValue };
560
+ }
561
+ const record = state.packages[delivery.name];
562
+ const packages = record ? { ...state.packages, [delivery.name]: { ...record, open: [...record.open, { count: delivery.count, startedBy: delivery.startedBy }].slice(-MAX_OPEN_STARTS) } } : state.packages;
563
+ this.#writeScript(runId, { ...state, packages, deliveries });
564
+ const { input: value, options } = JSON.parse(input.content) as { input: JsonValue; options: Record<string, JsonValue> };
565
+ return { start: { input: value, options, embedded: delivery.embedded, startedBy: delivery.startedBy, count: delivery.count } };
566
+ }
567
+ /** Checked before the state commits: every finish ends a different open start of this package. */
568
+ #finishesOf(runId: string, program: ActorProgramDefinition, value: unknown): readonly ScriptFinish[] {
569
+ const finishes = finishesOf(value);
570
+ const open = new Set(this.#script(runId).packages[program.name]?.open.map((entry) => entry.count) ?? []);
571
+ for (const [index, finish] of finishes.entries()) {
572
+ if (!open.has(finish.start) || finishes.findIndex((other) => other.start === finish.start) !== index)
573
+ throw new Error(`finish: start ${finish.start} of @${program.actorHandle} is not open; it was finished already, never reached the program, or is older than the last ${MAX_OPEN_STARTS} open starts.`);
574
+ }
575
+ return finishes;
576
+ }
577
+ #deliverResults(request: TurnRequest<"script">, program: ActorProgramDefinition, finishes: readonly ScriptFinish[]): void {
578
+ if (finishes.length === 0)
579
+ return;
580
+ const state = this.#script(request.runId);
581
+ const record = state.packages[program.name]!;
582
+ const finished = new Set(finishes.map((finish) => finish.start));
583
+ const starters = new Map(record.open.map((entry) => [entry.count, entry.startedBy]));
584
+ this.#writeScript(request.runId, { ...state, packages: { ...state.packages, [program.name]: { ...record, open: record.open.filter((entry) => !finished.has(entry.count)) } } });
585
+ for (const finish of finishes)
586
+ this.#deliverResult(request, program, finish, starters.get(finish.start)!);
587
+ }
588
+ /** The owner reads the summary in the chat, an LLM gets a short message, a TypeScript actor a result it recognizes. */
589
+ #deliverResult(request: TurnRequest<"script">, program: ActorProgramDefinition, finish: ScriptFinish, startedBy: string): void {
590
+ const starter = this.view(request.runId).actors.find((actor) => actor.id === startedBy);
591
+ if (!starter || starter.kind === "human") {
592
+ request.emit({ kind: "runtime", text: finish.summary ?? `Start ${finish.start} finished.` });
593
+ return;
594
+ }
595
+ const commandId = `run-script-result:${request.turnId}:${program.name}:${finish.start}`;
596
+ try {
597
+ if (starter.kind === "script") {
598
+ const state = this.#script(request.runId);
599
+ this.#writeScript(request.runId, { ...state, deliveries: [...state.deliveries, { commandId, actorId: starter.id, kind: "result" }] }, commandId);
600
+ }
601
+ this.runtime().enqueueInput({ actorId: this.ownerId(request.runId), commandId }, request.runId, {
602
+ actorId: starter.id, presentation: "background",
603
+ content: starter.kind === "agent" ? agentResultText(program.actorHandle, finish) : scriptResultContent(program.actorHandle, finish),
604
+ });
605
+ }
606
+ catch (error) {
607
+ request.emit({ kind: "runtime", text: `The result of start ${finish.start} did not reach @${starter.handle}: ${errorText(error)}` });
608
+ }
609
+ }
610
+ isScriptActor(runId: string, actorId: string): boolean {
611
+ return this.runtime().select(runId, (state) => {
612
+ const actor = state.actors.get(actorId);
613
+ if (!actor || actor.kind !== "script")
614
+ return false;
615
+ const script = [...state.pluginStates.values()].find((entry) => entry.pluginId === ACTOR_SCRIPT_STATE_ID && entry.scope.kind === "run");
616
+ return Object.hasOwn(scriptStateOf(script?.state).packages, actor.handle);
617
+ });
618
+ }
619
+ programOf(runId: string, actorId: string): ActorProgramIdentity | undefined {
620
+ const program = this.programs(runId).find((entry) => entry.actorId === actorId);
621
+ if (!program)
622
+ return undefined;
623
+ const record = this.#script(runId).packages[program.name];
624
+ const origin = record && this.#identityOf(program) === record.identity ? record.origin : { kind: "run" as const, installedBy: program.installedBy };
625
+ return { name: program.name, actorId, revision: program.revision, origin };
626
+ }
627
+ /** Makes a package active once, whatever state it is in; under a shared name only the package the host installed from that plugin, unchanged. */
628
+ ensure(context: CommandContext, runId: string, name: string, signal?: AbortSignal): Promise<{ actorId: string; handle: string; status: "active" | "restarted" | "activated" | "installed" }> {
629
+ this.#assertName(name);
630
+ return this.#withPackages(runId, [name], () => this.#ensure(context, runId, name, signal));
631
+ }
632
+ async #ensure(context: CommandContext, runId: string, name: string, signal?: AbortSignal) {
633
+ const outcome = (status: "active" | "restarted" | "activated" | "installed") => {
634
+ const program = this.#program(runId, name);
635
+ return { actorId: program.actorId, handle: program.actorHandle, status };
636
+ };
637
+ const shared = this.#options.sharedPackage(name);
638
+ const record = this.#script(runId).packages[name];
639
+ const assertShared = (identity: () => string): void => {
640
+ if (shared && !this.#hostShared(record, shared.pluginId, identity()))
641
+ throw foreignShared(name, shared.pluginId, record);
642
+ };
643
+ const active = this.programs(runId).find((program) => program.name === name);
644
+ if (active) {
645
+ assertShared(() => this.#identityOf(active));
646
+ return outcome("active");
647
+ }
648
+ const view = this.view(runId);
649
+ const stopped = view.pluginStates.flatMap((entry) => {
650
+ const program = entry.pluginId === ACTOR_PROGRAMS_STATE_ID && entry.scope.kind === "actor" ? (entry.state as unknown as ActorProgramState).program : null;
651
+ const actor = program?.name === name ? view.actors.find((candidate) => candidate.id === program.actorId) : undefined;
652
+ return program && actor && actor.kind !== "human" && actor.lifecycle.kind === "stopped" ? [{ program, actor }] : [];
653
+ })[0];
654
+ if (stopped) {
655
+ assertShared(() => this.#identityOf(stopped.program));
656
+ this.runtime().restartActor(context, runId, stopped.actor.id, "Actor program ensured");
657
+ return outcome("restarted");
658
+ }
659
+ const root = await this.workspaceDirectory(runId);
660
+ if (occupied(path.join(root, name))) {
661
+ const identity = packageIdentity(await projectSourceFiles(path.join(root, name)));
662
+ assertShared(() => identity);
663
+ await this.#activate(context, runId, name, true, signal);
664
+ return outcome("activated");
665
+ }
666
+ if (!shared)
667
+ throw new Error(`The package ${name} is neither in this run nor a shared actor package of this profile.`);
668
+ const claims = this.#claim(runId, [{ name, files: shared.files, origin: { kind: "shared", pluginId: shared.pluginId } }]);
669
+ try {
670
+ await this.#import(context, runId, name, shared.files, signal);
671
+ }
672
+ catch (error) {
673
+ this.#release(runId, claims);
674
+ throw error;
675
+ }
676
+ return outcome("installed");
677
+ }
302
678
  async check(runId: string, name: string, callerId: string, signal?: AbortSignal, reference?: string): Promise<CompiledProgram> {
303
679
  this.#assertName(name);
304
680
  const directory = path.join(await this.workspaceDirectory(runId), name);
@@ -314,7 +690,7 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
314
690
  if (!caller)
315
691
  throw new Error("Calling actor is missing.");
316
692
  const provisional: ExecutableActor = owner ?? {
317
- id: "pending", handle: name, displayName: pkg.title, kind: "script", grants: caller.grants.filter((grant) => grant.delegable),
693
+ id: "pending", handle: name, displayName: pkg.title, kind: "script", grants: inheritedGrants(caller),
318
694
  createdAt: new Date().toISOString(), createdBy: callerId, description: actorDescriptionOf(pkg.description) ?? null, execution: { driver: { kind: "script", config: {} }, workspacePath: null, turnTimeoutMs: null }, lifecycle: { kind: "idle", since: new Date().toISOString() }, usage: emptyUsage(), toolNames: null, openedToolNames: [],
319
695
  };
320
696
  const available = (await this.#options.agentToolsFor(runId, provisional.id, owner ? undefined : provisional)).map(descriptor);
@@ -387,7 +763,7 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
387
763
  return { directory, files, definition, clients, frameStyles, createActor: !owner, ...(backend ? { backendJavaScript: backend.javaScript } : {}) };
388
764
  }
389
765
  activate(context: CommandContext, runId: string, name: string, signal?: AbortSignal, reference?: string) {
390
- return this.#activate(context, runId, name, true, signal, reference);
766
+ return this.#withPackages(runId, [name], () => this.#activate(context, runId, name, true, signal, reference));
391
767
  }
392
768
  async #activate(context: CommandContext, runId: string, name: string, idle: boolean, signal?: AbortSignal, reference?: string) {
393
769
  const key = `${runId}\0${name}`;
@@ -477,7 +853,7 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
477
853
  else if (holder)
478
854
  throw new Error(`Handle @${name} already belongs to the ${holder.kind !== "human" && holder.lifecycle.kind === "stopped" ? "stopped" : "active"} actor ${holder.displayName}; only a stopped TypeScript actor is restarted on activation. Choose another package name.`);
479
855
  else
480
- this.runtime().createScriptActor(context, runId, { handle: name, displayName: definition.title, description: actorDescriptionOf(definition.description), grants: caller.grants.filter((grant) => grant.delegable), toolNames: null });
856
+ this.runtime().createScriptActor(context, runId, { handle: name, displayName: definition.title, description: actorDescriptionOf(definition.description), grants: inheritedGrants(caller), toolNames: null });
481
857
  const actor = this.resolveActor(runId, context.actorId, `@${name}`);
482
858
  definition.actorId = actor.id;
483
859
  definition.actorHandle = actor.handle;
@@ -503,9 +879,13 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
503
879
  }
504
880
  list(runId: string) { return this.programs(runId).map((program) => ({ name: program.name, actor: `@${program.actorHandle}`, functions: program.functions.map((fn) => fn.id), views: program.views.map((view) => ({ name: view.key, title: view.title, visible: view.visible })) })); }
505
881
  async remove(context: CommandContext, runId: string, name: string) {
506
- const program = this.#program(runId, name);
882
+ const packageName = this.#program(runId, name).name;
883
+ return this.#withPackages(runId, [packageName], () => this.#remove(context, runId, packageName, name));
884
+ }
885
+ async #remove(context: CommandContext, runId: string, packageName: string, name: string) {
886
+ const program = this.#program(runId, packageName);
507
887
  this.#assertIdle(runId, program.actorId);
508
- if (this.#installing.has(`${runId}\0${name}`))
888
+ if (this.#installing.has(`${runId}\0${packageName}`))
509
889
  throw new Error("The package is being activated.");
510
890
  const actor = this.actor(runId, program.actorId);
511
891
  const key = `${runId}\0${program.actorId}`;
@@ -654,8 +1034,11 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
654
1034
  await this.#serial(request.runId, request.agentId, signal, async () => {
655
1035
  const { program, descriptors } = await this.#bound(request.runId, request.agentId, undefined, signal, inputOf, descriptorsFor);
656
1036
  const binding: BackendBinding = { id: request.turnId, kind: "input", principal: { id: request.agentId, kind: "script" }, descriptorsFor, call: (name, input, index) => request.invoke(`${request.turnId}:capability:${index}`, name, input) };
657
- const result = await this.#backend(request.runId, program, { kind: "input", input: scriptInputOf(request.input) }, binding, descriptors, signal);
1037
+ const recognized = this.#recognize(request.runId, request.agentId, request.input);
1038
+ const result = await this.#backend(request.runId, program, { kind: "input", input: scriptInputOf(request.input), ...recognized }, binding, descriptors, signal);
1039
+ const finishes = this.#finishesOf(request.runId, program, result.result);
658
1040
  this.#commitState(request.runId, request.agentId, result.state, result.initial);
1041
+ this.#deliverResults(request, program, finishes);
659
1042
  for (const text of result.logs)
660
1043
  request.emit({ kind: "runtime", text });
661
1044
  });
@@ -787,12 +1170,25 @@ export class ActorProgramRuntime implements ActorProgramsService, ActorProgramEx
787
1170
  }
788
1171
  }
789
1172
  waitForRunSettlement(runId: string): Promise<void> { return Promise.allSettled([...this.#active.values()].filter((entry) => entry.runId === runId).map((entry) => entry.completed).concat([...this.#queues].filter(([key]) => key.startsWith(`${runId}\0`)).map(([, queue]) => queue))).then(() => undefined); }
790
- async prepareSession(runId: string): Promise<void> { this.#knownRuns.add(runId); if (!this.runtime().listRuns().some(run => run.id === runId))
791
- return; for (const invocation of this.#invocations(runId).invocations)
792
- if (invocation.status === "queued" || invocation.status === "running")
793
- this.#replaceInvocation(runId, { ...invocation, status: "cancelled", error: "The server was stopped during the call.", finishedAt: new Date().toISOString() }); }
1173
+ /** Once per run and server process: calls a previous process left open are cancelled, calls of this process keep running. */
1174
+ async prepareSession(runId: string): Promise<void> {
1175
+ this.#knownRuns.add(runId);
1176
+ if (this.#recovered.has(runId) || !this.runtime().listRuns().some(run => run.id === runId))
1177
+ return;
1178
+ this.#recovered.add(runId);
1179
+ for (const invocation of this.#invocations(runId).invocations)
1180
+ if ((invocation.status === "queued" || invocation.status === "running") && !this.#active.has(invocation.id))
1181
+ this.#replaceInvocation(runId, { ...invocation, status: "cancelled", error: "The server was stopped during the call.", finishedAt: new Date().toISOString() });
1182
+ }
794
1183
  async stopSession(runId: string, _signal?: AbortSignal): Promise<void> { await this.stopRun(runId); }
795
1184
  async disposeRun(runId: string): Promise<void> { await this.stopRun(runId); }
796
- async deleteSession(runId: string): Promise<void> { await this.stopRun(runId); await rm(this.#options.directoryFor(runId), { recursive: true, force: true }); }
1185
+ async deleteSession(runId: string): Promise<void> {
1186
+ await this.stopRun(runId);
1187
+ this.#recovered.delete(runId);
1188
+ for (const directory of this.#identities.keys())
1189
+ if (directory.startsWith(`${this.#options.directoryFor(runId)}${path.sep}`))
1190
+ this.#identities.delete(directory);
1191
+ await rm(this.#options.directoryFor(runId), { recursive: true, force: true });
1192
+ }
797
1193
  async shutdown(): Promise<void> { this.#shuttingDown = true; await Promise.allSettled([...this.#knownRuns].map((runId) => this.stopRun(runId))); }
798
1194
  }
@@ -0,0 +1,54 @@
1
+ import type { JsonValue } from "@ragents/engine";
2
+ import type { ActorScriptState } from "@ragents/host/plugin-support/actor-programs/contract.js";
3
+
4
+ export const MAX_OPEN_STARTS = 50;
5
+ const MAX_RESULT_CHARACTERS = 32_000;
6
+ const MAX_SUMMARY_LENGTH = 1_000;
7
+ const SHOWN_RESULT_CHARACTERS = 4_000;
8
+
9
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
10
+
11
+ const isOrigin = (value: unknown): boolean =>
12
+ isRecord(value) && (value.kind === "script" && typeof value.entryId === "string" || value.kind === "shared" && typeof value.pluginId === "string");
13
+
14
+ /** An older or unknown shape counts as empty, and so does a package record of an older shape: such packages have no recorded origin. */
15
+ export const scriptStateOf = (value: unknown): ActorScriptState => {
16
+ if (!isRecord(value) || value.version !== 2 || !isRecord(value.packages) || !Array.isArray(value.deliveries))
17
+ return { version: 2, packages: {}, deliveries: [] };
18
+ const packages = Object.entries(value.packages).filter(([, entry]) => isRecord(entry) && isOrigin(entry.origin) && typeof entry.identity === "string");
19
+ return { ...value as unknown as ActorScriptState, packages: Object.fromEntries(packages) as ActorScriptState["packages"] };
20
+ };
21
+
22
+ export interface ScriptFinish {
23
+ readonly start: number;
24
+ readonly result: JsonValue;
25
+ readonly summary?: string;
26
+ }
27
+
28
+ /** What a program's turn handler returned through context.finish; a handler built before finish returns nothing. */
29
+ export const finishesOf = (value: unknown): readonly ScriptFinish[] => {
30
+ if (value === undefined || value === null) return [];
31
+ if (!isRecord(value) || !Array.isArray(value.finishes)) throw new Error("The program returned an invalid finish list.");
32
+ return value.finishes.map((entry): ScriptFinish => {
33
+ if (!isRecord(entry) || !Number.isInteger(entry.start) || (entry.start as number) < 1) throw new Error("finish needs the count of an open start.");
34
+ const result = (entry.result ?? null) as JsonValue;
35
+ if (JSON.stringify(result).length > MAX_RESULT_CHARACTERS) throw new Error(`The result of finish is larger than ${MAX_RESULT_CHARACTERS} characters as JSON.`);
36
+ if (entry.summary !== undefined && (typeof entry.summary !== "string" || entry.summary.length > MAX_SUMMARY_LENGTH)) {
37
+ throw new Error(`The summary of finish must be a text of at most ${MAX_SUMMARY_LENGTH} characters.`);
38
+ }
39
+ return { start: entry.start as number, result, ...(typeof entry.summary === "string" ? { summary: entry.summary } : {}) };
40
+ });
41
+ };
42
+
43
+ /** The result as a short message for an LLM starter: summary line, then the compact result. */
44
+ export const agentResultText = (handle: string, finish: ScriptFinish): string => {
45
+ const headline = `Run script @${handle} finished start ${finish.start}${finish.summary ? `: ${finish.summary}` : "."}`;
46
+ if (finish.result === null) return headline;
47
+ const json = JSON.stringify(finish.result);
48
+ const shown = json.length > SHOWN_RESULT_CHARACTERS ? `${json.slice(0, SHOWN_RESULT_CHARACTERS)}... (shortened, ${json.length} characters)` : json;
49
+ return `${headline}\nResult: ${shown}`;
50
+ };
51
+
52
+ /** The result for a TypeScript starter: onResult gets it parsed, onInput as this JSON. */
53
+ export const scriptResultContent = (handle: string, finish: ScriptFinish): string =>
54
+ JSON.stringify({ handle, count: finish.start, result: finish.result, ...(finish.summary !== undefined ? { summary: finish.summary } : {}) });