@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.
- package/apps/server/package.json +1 -1
- package/apps/server/src/api/contracts.ts +19 -1
- package/apps/server/src/api/core-methods.ts +12 -0
- package/apps/server/src/chat-handler.ts +19 -0
- package/apps/server/src/host-api.json +2 -0
- package/apps/server/src/host-api.ts +2 -2
- package/apps/server/src/plugin-build/build.ts +1 -1
- package/apps/server/src/plugin-support/actor-programs/app-project.ts +62 -18
- package/apps/server/src/plugin-support/actor-programs/client-contracts.ts +47 -8
- package/apps/server/src/plugin-support/actor-programs/contract.ts +18 -1
- package/apps/server/src/plugin-support/actor-programs/service.ts +22 -1
- package/apps/server/src/plugin-support/plugin-folder.ts +14 -1
- package/apps/server/src/plugin-support/run-scripts.ts +27 -2
- package/apps/server/src/profile/compose.ts +2 -0
- package/apps/server/src/provider.ts +23 -4
- package/apps/server/src/ragents/chat-projection.ts +7 -0
- package/apps/server/src/ragents/global-chat.ts +5 -1
- package/apps/server/src/ragents/run-script-tools.ts +66 -0
- package/apps/server/src/ragents/session.ts +122 -60
- package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-BslIwn_y.js → highlighted-body-KPVGNVTW-BwD4uYEe.js} +1 -1
- package/apps/web/dist/assets/{main-BUN8m8SW.js → main-KBrc5Q77.js} +1 -1
- package/apps/web/dist/assets/{mermaid-HWGCJPDP-B1erZlFM.js → mermaid-HWGCJPDP-CSCkaArr.js} +88 -88
- package/apps/web/dist/assets/runPanel-BjG5NBKm.js +26 -0
- package/apps/web/dist/help/guide-clients.html +21 -4
- package/apps/web/dist/help/guide-clients.md +23 -4
- package/apps/web/dist/help/guide-plugins.html +12 -2
- package/apps/web/dist/help/guide-plugins.md +9 -2
- package/apps/web/dist/help/guide-programs.html +8 -2
- package/apps/web/dist/help/guide-programs.md +9 -2
- package/apps/web/dist/help/guide-runtime.html +3 -2
- package/apps/web/dist/help/guide-runtime.md +3 -2
- package/apps/web/dist/help/index.html +1 -1
- package/apps/web/dist/help/mini-app.css +7 -2
- package/apps/web/dist/host-web.json +6 -6
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/run-panel.html +2 -2
- package/apps/web/package.json +1 -1
- package/apps/web/src/run-panel/RunPanelApp.tsx +1 -1
- package/apps/web/src/run-panel/RunPanelMenu.tsx +47 -11
- package/bundles/ragents.activity/ragents-bundle.json +1 -1
- package/bundles/ragents.actor-programs/ragents-bundle.json +4 -2
- package/bundles/ragents.actor-programs/server/index.js +437 -24
- package/bundles/ragents.actor-programs/server/index.js.map +4 -4
- package/bundles/ragents.actor-programs/web/index.js.map +1 -1
- package/bundles/ragents.ask/ragents-bundle.json +2 -2
- package/bundles/ragents.ask/server/index.js +1 -0
- package/bundles/ragents.ask/server/index.js.map +2 -2
- package/bundles/ragents.browser/ragents-bundle.json +1 -1
- package/bundles/ragents.documents/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
- package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
- package/bundles/ragents.orchestration/ragents-bundle.json +7 -4
- package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js +11 -0
- package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js.map +7 -0
- package/bundles/ragents.orchestration/server/exports/server/contract.js +9 -0
- package/bundles/ragents.orchestration/server/exports/server/contract.js.map +7 -0
- package/bundles/ragents.orchestration/server/index.js +49 -3
- package/bundles/ragents.orchestration/server/index.js.map +2 -2
- package/bundles/ragents.orchestration/surface.hbs +5 -0
- package/bundles/ragents.overseer/ragents-bundle.json +1 -1
- package/bundles/ragents.processes/ragents-bundle.json +1 -1
- package/bundles/ragents.product/ragents-bundle.json +1 -1
- package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
- package/bundles/ragents.reference/actors/notebook/package.json +16 -0
- package/bundles/ragents.reference/actors/notebook/src/client.tsx +31 -0
- package/bundles/ragents.reference/actors/notebook/src/server.ts +27 -0
- package/bundles/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
- package/bundles/ragents.reference/ragents-bundle.json +4 -3
- package/bundles/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
- package/bundles/ragents.reference/run-scripts/quick-note/package.json +9 -0
- package/bundles/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
- package/bundles/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
- package/bundles/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
- package/bundles/ragents.reference/run-scripts/run-roster/package.json +9 -0
- package/bundles/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
- package/bundles/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
- package/bundles/ragents.todo/ragents-bundle.json +1 -1
- package/bundles/ragents.transcript/ragents-bundle.json +1 -1
- package/bundles/ragents.watch/ragents-bundle.json +1 -1
- package/bundles/ragents.workspace/ragents-bundle.json +1 -1
- package/package.json +3 -3
- package/packages/ragents/src/domain/vocabulary.ts +4 -0
- package/packages/ragents/src/index.ts +3 -1
- package/packages/ragents/src/plugin-host.ts +62 -10
- package/packages/ragents/src/plugin-types.ts +11 -0
- package/packages/ragents/src/runtime/guards.ts +2 -1
- package/packages/ragents/src/typescript/async-compiler.ts +11 -1
- package/plugins/ragents.actor-programs/server/index.ts +3 -0
- package/plugins/ragents.actor-programs/server/runtime.ts +424 -28
- package/plugins/ragents.actor-programs/server/script-state.ts +54 -0
- package/plugins/ragents.actor-programs/server/tool-contributor.ts +2 -0
- package/plugins/ragents.ask/server/ask-tool.ts +1 -0
- package/plugins/ragents.orchestration/ragents-plugin.json +1 -0
- package/plugins/ragents.orchestration/server/contract.ts +14 -0
- package/plugins/ragents.orchestration/server/index.ts +4 -1
- package/plugins/ragents.orchestration/server/surface-tool.ts +58 -2
- package/plugins/ragents.orchestration/surface.hbs +5 -0
- package/plugins/ragents.reference/actors/notebook/package.json +16 -0
- package/plugins/ragents.reference/actors/notebook/src/client.tsx +31 -0
- package/plugins/ragents.reference/actors/notebook/src/server.ts +27 -0
- package/plugins/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
- package/plugins/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
- package/plugins/ragents.reference/run-scripts/quick-note/package.json +9 -0
- package/plugins/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
- package/plugins/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
- package/plugins/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
- package/plugins/ragents.reference/run-scripts/run-roster/package.json +9 -0
- package/plugins/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
- package/plugins/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
- package/scripts/agent/agent-cli.ts +46 -2
- package/scripts/package/ragents.mjs +3 -1
- 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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
|
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
|
|
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
|
|
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${
|
|
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
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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> {
|
|
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 } : {}) });
|