@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
@@ -19,7 +19,7 @@
19
19
  "@ragents/engine": "workspace:*",
20
20
  "@ragents/workspace-executor": "workspace:*",
21
21
  "handlebars": "^4.7.9",
22
- "quassel": "^0.4.0",
22
+ "quassel": "^0.4.2",
23
23
  "tsx": "^4.20.0",
24
24
  "typebox": "^1.3.13",
25
25
  "vscode-jsonrpc": "^9.0.2"
@@ -44,6 +44,24 @@ export const coreContracts = {
44
44
  input: Type.Object({ runId }, { additionalProperties: false }),
45
45
  result: Type.Null(),
46
46
  }),
47
+ scripts: defineOperation({
48
+ id: "ragents.runs.scripts",
49
+ description: "The run scripts the caller may start, and for a running run whether each can start there now: available, otherwise reason (not embeddable, a fixed start option differs from the run's). Rights: runs.read, runs.write.",
50
+ input: Type.Object({ runId }, { additionalProperties: false }),
51
+ result: Type.Array(Type.Object({
52
+ id: Type.String(),
53
+ title: Type.String(),
54
+ description: Type.String(),
55
+ available: Type.Boolean(),
56
+ reason: Type.Optional(Type.String()),
57
+ }, { additionalProperties: false })),
58
+ }),
59
+ startScript: defineOperation({
60
+ id: "ragents.runs.startScript",
61
+ description: "Start a run script and wait until its actor has the start input; errors come back to the caller instead of the chat. A new run starts as with ragents.chat.start. In a running run only a script whose RUN.md sets embeddable: true starts, its fixed start options must match the run's, and the primary actor stays; a repeated start reuses the script's actor. Result: the script actor and which start of its package in the run this was. Rights: runs.read, runs.write and the template's release.",
62
+ input: Type.Object({ runId, entry: Type.String({ minLength: 1 }), input: Type.Optional(Type.Any()) }, { additionalProperties: false }),
63
+ result: Type.Object({ actorId: Type.String(), handle: Type.String(), count: Type.Integer({ minimum: 1 }) }, { additionalProperties: false }),
64
+ }),
47
65
  },
48
66
  chat: {
49
67
  send: defineOperation({
@@ -66,7 +84,7 @@ export const coreContracts = {
66
84
  }),
67
85
  start: defineOperation({
68
86
  id: "ragents.chat.start",
69
- description: "Start a run through a script template, without a message; the start options the template fixes apply. Rights: runs.read, runs.write and the template's release.",
87
+ description: "Start a run through a script template, without a message; the start options the template fixes apply. Returns once the start is accepted; progress and errors appear in the chat. In a running run it starts an embeddable script like ragents.runs.startScript. Rights: runs.read, runs.write and the template's release.",
70
88
  input: Type.Object({ runId, entry: Type.String({ minLength: 1 }), input: Type.Optional(Type.Any()) }, { additionalProperties: false }),
71
89
  result: Type.Null(),
72
90
  }),
@@ -114,6 +114,18 @@ export const coreMethods = (sources: CoreMethodSources): MethodContribution[] =>
114
114
  current.start(trimmed, input === undefined ? null : input, userOf(access));
115
115
  return null;
116
116
  }),
117
+ implement(coreContracts.runs.scripts, async ({ runId }, { access }) => {
118
+ const current = await session(access, runId, "write");
119
+ if (!current.runScripts) throw new DomainError("script-start-unavailable", "This run cannot start run scripts.", 404);
120
+ return current.runScripts(sources.plugins.startEntries.describe().filter((entry) => canStartEntry(access, entry.id)), userIdOf(access));
121
+ }),
122
+ implement(coreContracts.runs.startScript, async ({ runId, entry, input }, { access }) => {
123
+ const trimmed = entry.trim();
124
+ if (!canStartEntry(access, trimmed)) throw new DomainError("access-denied", "This setup is not enabled for this access.", 403);
125
+ const current = await session(access, runId, "write");
126
+ if (!current.startAndWait) throw new DomainError("script-start-unavailable", "This run cannot start run scripts.", 404);
127
+ return current.startAndWait(trimmed, input === undefined ? null : input, userOf(access));
128
+ }),
117
129
  implement(coreContracts.chat.stop, async ({ runId }, { access }) => {
118
130
  const current = await session(access, runId, "stop");
119
131
  await current.stop();
@@ -1,5 +1,6 @@
1
1
  /** The contracts of the runs that core methods and delivery work against. */
2
2
  import type { ChatAttachment, ChatAttachmentInput, ChatEvent } from "quassel/events";
3
+ import type { PublicStartEntry } from "@ragents/engine";
3
4
  import type { ActorConversations } from "./ragents/actor-chat-history.js";
4
5
 
5
6
  export interface SessionInfo {
@@ -18,6 +19,22 @@ export interface ChatUser {
18
19
  }
19
20
 
20
21
  /** What the HTTP adapter needs from a run. */
22
+ /** A started run script: its actor and which start of its package in the run this was. */
23
+ export interface StartedScript {
24
+ readonly actorId: string;
25
+ readonly handle: string;
26
+ readonly count: number;
27
+ }
28
+
29
+ /** A run script as a run lists it: whether it can start there now, and otherwise why not. */
30
+ export interface RunScriptListing {
31
+ readonly id: string;
32
+ readonly title: string;
33
+ readonly description: string;
34
+ readonly available: boolean;
35
+ readonly reason?: string;
36
+ }
37
+
21
38
  export interface ChatSessionLike {
22
39
  readonly running: boolean;
23
40
  subscribe(listener: (event: ChatEvent) => void): () => void;
@@ -27,6 +44,8 @@ export interface ChatSessionLike {
27
44
  capabilities?(actor: string, userId: string | null): Promise<{ input: string[]; model: string }>;
28
45
  attachment?(artifactId: string): { attachment: ChatAttachment; content: Uint8Array };
29
46
  start(entryId: string, input: unknown, user?: ChatUser): void;
47
+ startAndWait?(entryId: string, input: unknown, user?: ChatUser, startedBy?: string): Promise<StartedScript>;
48
+ runScripts?(entries: readonly PublicStartEntry[], userId: string | null): RunScriptListing[];
30
49
  stop(): void | Promise<void>;
31
50
  }
32
51
 
@@ -25,6 +25,7 @@
25
25
  "holdsUsable",
26
26
  "implement",
27
27
  "implementChannel",
28
+ "inheritedGrants",
28
29
  "isRunId",
29
30
  "isThinkingLevel",
30
31
  "modelToolDescriptors",
@@ -77,6 +78,7 @@
77
78
  "@ragents/host/plugin-support/actor-programs/app-project": [
78
79
  "compileAppBackend",
79
80
  "installServerSdk",
81
+ "packageIdentity",
80
82
  "prepareAppProject",
81
83
  "prepareAppWorkspace",
82
84
  "projectSourceFiles",
@@ -13,7 +13,7 @@ export const hostApi: { readonly server: HostApiModules; readonly web: HostApiMo
13
13
  "DomainError", "RPC_ERROR_CODES", "RpcError", "ScriptDriver", "ToolRegistry", "actorByHandle",
14
14
  "actorByReference", "actorDescriptionMaxLength", "actorInputSchema", "agentTools", "assertJsonValue", "builtinProfiles", "canonicalHash",
15
15
  "compileVirtualTypeScriptAsync", "defineRunFunction", "defineToolAvailability", "emptyUsage",
16
- "enqueueActorInput", "eventResultSchemaOf", "handleKey", "holdsUsable", "implement", "implementChannel",
16
+ "enqueueActorInput", "eventResultSchemaOf", "handleKey", "holdsUsable", "implement", "implementChannel", "inheritedGrants",
17
17
  "isRunId", "isThinkingLevel", "modelToolDescriptors", "pluginStateAt", "runCapabilityBindingHash",
18
18
  "runCapabilityContractHash", "schemaComplaints", "scriptInputOf", "serviceToken",
19
19
  ],
@@ -31,7 +31,7 @@ export const hostApi: { readonly server: HostApiModules; readonly web: HostApiMo
31
31
  ],
32
32
  "@ragents/host/host-version": ["readHostApiVersion", "readHostVersion", "readPackageVersion"],
33
33
  "@ragents/host/plugin-support/actor-programs/app-project": [
34
- "compileAppBackend", "installServerSdk", "prepareAppProject", "prepareAppWorkspace", "projectSourceFiles",
34
+ "compileAppBackend", "installServerSdk", "packageIdentity", "prepareAppProject", "prepareAppWorkspace", "projectSourceFiles",
35
35
  "readAppPackage", "typecheckServerProject",
36
36
  ],
37
37
  "@ragents/host/plugin-support/actor-programs/client-compiler": ["compileClientProject", "installClientSdk"],
@@ -39,7 +39,7 @@ export interface PluginBuildOptions {
39
39
  readonly onlyOutdated?: boolean;
40
40
  }
41
41
 
42
- const DEFAULT_ASSET_FOLDERS = ["skills", "prompts", "run-scripts"] as const;
42
+ const DEFAULT_ASSET_FOLDERS = ["skills", "prompts", "run-scripts", "actors"] as const;
43
43
  const SKIPPED_ASSET_FILES = new Set([".DS_Store"]);
44
44
  const WEB_FILE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif", ".ico", ".woff", ".woff2", ".ttf", ".otf"];
45
45
  const CODE_EXTENSION = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
@@ -13,7 +13,7 @@ import { runtimeLibraries, webRuntimeLibraries } from "./runtime-libraries.js";
13
13
  const webRequire = createRequire(new URL("../../../../web/package.json", import.meta.url));
14
14
  const rootRequire = createRequire(new URL("../../../../../package.json", import.meta.url));
15
15
  const serverRequire = createRequire(new URL("../../../package.json", import.meta.url));
16
- const serverTypeRoot = fileURLToPath(new URL("../../../node_modules/@types", import.meta.url));
16
+ const serverTypeRoot = path.dirname(path.dirname(serverRequire.resolve("@types/node/package.json")));
17
17
  const libraryDirectory = (name: string): string => {
18
18
  const resolver = (webRuntimeLibraries as readonly string[]).includes(name) ? webRequire
19
19
  : name === "@types/node" || name === "tsx" ? serverRequire : rootRequire;
@@ -107,8 +107,10 @@ export function createTestContextWithContracts(options, contracts) {
107
107
  if(!Value.Check(contract.resultSchema,result))throw new Error("Test response violates the contract of "+name+": "+schemaComplaints(contract.resultSchema,result,"result"));
108
108
  return result;
109
109
  }};
110
+ const finished = [];
110
111
  return {...createRunContext({runId:"test",invocationId:"test",invocationKind:"tool",principal:{id:"test",kind:"service"},state:statePort,capabilities,log:()=>{},signal:controller.signal}),
111
- actor:{id:"test-actor",handle:"test"},std:createScriptStd({turnStartedAt:"2026-01-01T00:00:00.000Z",idPrefix:"test",state:statePort,capabilities})};
112
+ actor:{id:"test-actor",handle:"test"},std:createScriptStd({turnStartedAt:"2026-01-01T00:00:00.000Z",idPrefix:"test",state:statePort,capabilities}),
113
+ finished,finish:(result,options={})=>{assertJsonValue(result===undefined?null:result,"Test finish result");finished.push({result:result===undefined?null:result,...options});}};
112
114
  }`, resolveDir:process.cwd(),sourcefile:"actor-testing.ts",loader:"ts"},
113
115
  bundle:true,platform:"node",format:"esm",target:"node22",packages:"external",write:false,
114
116
  }).then(result=>result.outputFiles![0]!.text);
@@ -134,6 +136,8 @@ export interface RunContext<State> {
134
136
  readonly functions: {readonly [Name in keyof CapabilityContracts]: (...args: {} extends CapabilityContracts[Name]['input'] ? [input?: CapabilityContracts[Name]['input']] : [input: CapabilityContracts[Name]['input']]) => Promise<CapabilityContracts[Name]['output']>};
135
137
  log(value: unknown): void;
136
138
  throwIfAborted(): void;
139
+ /** Ends a start of this run script with a JSON result; the host delivers it once to whoever started it. Only in onStart, onInput and onResult; outside onStart, name the start. */
140
+ finish(result: unknown, options?: { readonly summary?: string; readonly start?: number }): void;
137
141
  }
138
142
  ${scriptStdDeclarations}
139
143
  export interface ActorInput {
@@ -142,10 +146,15 @@ export interface ActorInput {
142
146
  readonly event: { readonly type: string; readonly eventId: string; readonly sequence: number; readonly occurredAt: string;
143
147
  readonly sourceActorId: string | null; readonly sourceActorHandle: string | null; readonly payload: {readonly text?:string; readonly [key:string]:unknown} } | null;
144
148
  }
149
+ export interface ActorStart {
150
+ readonly input: unknown; readonly options: Readonly<Record<string, unknown>>;
151
+ readonly embedded: boolean; readonly startedBy: string; readonly count: number;
152
+ }
153
+ export interface ActorResult { readonly handle: string; readonly count: number; readonly result: unknown; readonly summary?: string }
145
154
  export interface ActorFunction { label: string; description?: string; input: TSchema; output: TSchema; capabilities?: readonly string[]; confirmation?: string; tool?: { name: string; targets?: readonly string[]; card?: boolean } }
146
155
  export interface ActorContract { state: TSchema; functions: Readonly<Record<string, ActorFunction>>; input?: {capabilities?: readonly string[]} }
147
156
  export type ActorFunctions<C extends ActorContract> = { [K in keyof C['functions']]: (input: Static<C['functions'][K]['input']>, context: RunContext<Static<C['state']>>) => Static<C['functions'][K]['output']> | Promise<Static<C['functions'][K]['output']>> };
148
- export type ActorImplementation<C extends ActorContract> = {functions: ActorFunctions<C>} & (C extends {input: unknown} ? {onInput: (input: ActorInput, context: RunContext<Static<C['state']>>) => void | Promise<void>} : {onInput?: never});
157
+ export type ActorImplementation<C extends ActorContract> = {functions: ActorFunctions<C>} & (C extends {input: unknown} ? {onInput: (input: ActorInput, context: RunContext<Static<C['state']>>) => void | Promise<void>; onStart?: (start: ActorStart, context: RunContext<Static<C['state']>>) => void | Promise<void>; onResult?: (result: ActorResult, context: RunContext<Static<C['state']>>) => void | Promise<void>} : {onInput?: never; onStart?: never; onResult?: never});
149
158
  export declare function defineActor<const C extends ActorContract>(contract: C, implementation: ActorImplementation<C>): {contract:C} & ActorImplementation<C>;
150
159
  `;
151
160
  };
@@ -193,24 +202,45 @@ export const installServerSdk = async (directory: string, capabilities: readonly
193
202
  await writeFile(path.join(sdk, "index.js"), sdkRuntime);
194
203
  await writeFile(path.join(sdk, "index.d.ts"), declarations);
195
204
  await writeFile(path.join(sdk, "testing.js"), (await testingRuntime()) + `\nexport function createTestContext(options) { return createTestContextWithContracts(options, ${JSON.stringify(capabilities)}); }\n`);
196
- await writeFile(path.join(sdk, "testing.d.ts"), "import type { RunContext, CapabilityContracts } from './index.js';\ntype Mocks = {[Name in keyof CapabilityContracts]?: (input: CapabilityContracts[Name]['input']) => CapabilityContracts[Name]['output'] | Promise<CapabilityContracts[Name]['output']>};\nexport declare function createTestContext<State>(options: {state: State; functions?: Mocks}): RunContext<State>;\n");
205
+ await writeFile(path.join(sdk, "testing.d.ts"), "import type { RunContext, CapabilityContracts } from './index.js';\ntype Mocks = {[Name in keyof CapabilityContracts]?: (input: CapabilityContracts[Name]['input']) => CapabilityContracts[Name]['output'] | Promise<CapabilityContracts[Name]['output']>};\ntype Finished = {readonly result: unknown; readonly summary?: string; readonly start?: number};\nexport declare function createTestContext<State>(options: {state: State; functions?: Mocks}): RunContext<State> & {readonly finished: readonly Finished[]};\n");
197
206
  };
198
207
 
208
+ /** The tsconfig files the host writes into a package that brings none of its own. */
209
+ const generatedConfigs = (pkg: Pick<AppPackage, "backend" | "views">): Readonly<Record<string, string>> => ({
210
+ "tsconfig.client.json": json({ extends: "./node_modules/@ragents/client/tsconfig.json", include: (pkg.views ?? []).map(view => view.client) }),
211
+ "tsconfig.server.json": json({ compilerOptions: {
212
+ target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true,
213
+ skipLibCheck: true, types: ["node"], lib: ["ES2022"], allowImportingTsExtensions: true,
214
+ }, include: [...(pkg.backend ? [pkg.backend] : []), "tests/**/*.ts"] }),
215
+ "tsconfig.json": json({ files: [], references: [{ path: "./tsconfig.client.json" }, { path: "./tsconfig.server.json" }] }),
216
+ });
217
+
199
218
  export const prepareAppProject = async (directory: string): Promise<void> => {
200
219
  await prepareAppDependencies(directory);
201
220
  await installServerSdk(directory);
202
221
  const pkg = await readAppPackage(directory);
203
- const writeConfig = async (name: string, value: unknown): Promise<void> => {
204
- await writeFile(path.join(directory, name), json(value), { flag: "wx" }).catch((error: NodeJS.ErrnoException) => {
222
+ for (const [name, content] of Object.entries(generatedConfigs(pkg))) {
223
+ await writeFile(path.join(directory, name), content, { flag: "wx" }).catch((error: NodeJS.ErrnoException) => {
205
224
  if (error.code !== "EEXIST") throw error;
206
225
  });
207
- };
208
- await writeConfig("tsconfig.client.json", { extends: "./node_modules/@ragents/client/tsconfig.json", include: (pkg.views ?? []).map(view => view.client) });
209
- await writeConfig("tsconfig.server.json", { compilerOptions: {
210
- target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true,
211
- skipLibCheck: true, types: ["node"], lib: ["ES2022"], allowImportingTsExtensions: true,
212
- }, include: [...(pkg.backend ? [pkg.backend] : []), "tests/**/*.ts"] });
213
- await writeConfig("tsconfig.json", { files: [], references: [{ path: "./tsconfig.client.json" }, { path: "./tsconfig.server.json" }] });
226
+ }
227
+ };
228
+
229
+ /** What a package is, independent of the tsconfig files the host generated into it: two packages with the same identity have the same sources. */
230
+ export const packageIdentity = (files: readonly { path: string; content: string }[]): string => {
231
+ const normalized = files.map((file) => ({ path: file.path.split(path.sep).join("/"), content: file.content }));
232
+ const generated = generatedConfigsOf(normalized.find((file) => file.path === "package.json")?.content);
233
+ return canonicalHash(normalized.filter((file) => generated[file.path] !== file.content).sort((left, right) => left.path.localeCompare(right.path)));
234
+ };
235
+
236
+ /** A package.json that does not parse has no generated configs; its activation reports the error. */
237
+ const generatedConfigsOf = (packageJson: string | undefined): Readonly<Record<string, string>> => {
238
+ try {
239
+ const pkg = (JSON.parse(packageJson ?? "{}") as { ragents?: Pick<AppPackage, "backend" | "views"> }).ragents;
240
+ return pkg ? generatedConfigs(pkg) : {};
241
+ } catch {
242
+ return {};
243
+ }
214
244
  };
215
245
 
216
246
  export const readAppPackage = async (directory: string): Promise<AppPackage> => {
@@ -261,13 +291,27 @@ export const compileAppBackend = async (options: {
261
291
  export const describe = () => {
262
292
  for (const name of Object.keys(actor.contract.functions ?? {})) if (typeof actor.functions?.[name] !== 'function') throw new Error('Missing function ' + name);
263
293
  if (Boolean(actor.contract.input) !== (typeof actor.onInput === 'function')) throw new Error('input and onInput must be present together.');
294
+ for (const name of ['onStart', 'onResult']) if (actor[name] !== undefined && (typeof actor[name] !== 'function' || !actor.contract.input)) throw new Error(name + ' needs input and onInput.');
264
295
  return JSON.parse(JSON.stringify(actor.contract));
265
296
  };
266
- export const handle = (request, context) => {
267
- if (request.kind === 'input') { if (!actor.onInput) throw new Error('This actor does not process input.'); return actor.onInput(request.input, context); }
268
- const handler = actor.functions[request.functionId];
269
- if (typeof handler !== 'function') throw new Error('Unknown actor function ' + request.functionId);
270
- return handler(request.input, context);
297
+ export const handle = async (request, context) => {
298
+ if (request.kind !== 'input') {
299
+ const handler = actor.functions[request.functionId];
300
+ if (typeof handler !== 'function') throw new Error('Unknown actor function ' + request.functionId);
301
+ return handler(request.input, { ...context, finish: () => { throw new Error('finish works only in onStart, onInput and onResult; a function passes the result as an input to its own actor.'); } });
302
+ }
303
+ if (!actor.onInput) throw new Error('This actor does not process input.');
304
+ const finishes = [];
305
+ const finish = (result, options = {}) => {
306
+ const start = options.start ?? request.start?.count;
307
+ if (!Number.isInteger(start)) throw new Error('finish needs options.start, the count of the start it ends, outside onStart.');
308
+ finishes.push({ start, result: result === undefined ? null : result, ...(options.summary === undefined ? {} : { summary: options.summary }) });
309
+ };
310
+ const handlerContext = { ...context, finish };
311
+ if (request.result && actor.onResult) await actor.onResult(request.result, handlerContext);
312
+ else if (request.start && actor.onStart) await actor.onStart(request.start, handlerContext);
313
+ else await actor.onInput(request.input, handlerContext);
314
+ return { finishes };
271
315
  };`, resolveDir: options.directory, sourcefile: "actor-entry.ts", loader: "ts" },
272
316
  absWorkingDir: options.directory, platform: "node", format: "esm", target: "node22", bundle: true, packages: "external", write: false,
273
317
  });
@@ -1,16 +1,55 @@
1
+ import { realpathSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import ts from "typescript";
4
5
 
5
- function readContracts(repoRoot: string): { names: string[]; files: Record<string, string> } {
6
- const filename = path.join(repoRoot, "apps/web/src/actor-programs/client-ui/contracts.d.ts");
6
+ export function clientUiContractPaths(directory: string, paths: typeof path.posix = path) {
7
+ const canonical = (name: string) => paths.resolve(name).replaceAll(paths.sep, "/").replace(/^[a-z]:/, (drive) => drive.toUpperCase());
8
+ const repoRoot = canonical(directory);
9
+ const root = canonical(paths.join(paths.parse(repoRoot).root, "__ragents_ui_contract__"));
10
+ const contains = (base: string, name: string) => name === base || name.startsWith(`${base}/`);
11
+ const physical = (name: string) => {
12
+ const file = canonical(name);
13
+ return paths.normalize(contains(root, file) ? repoRoot + file.slice(root.length) : file);
14
+ };
15
+ const virtual = (name: string) => {
16
+ const file = canonical(name);
17
+ return contains(repoRoot, file) ? root + file.slice(repoRoot.length) : file;
18
+ };
19
+ return { root, physical, virtual, isVirtual: (name: string) => contains(root, canonical(name)) };
20
+ }
21
+
22
+ function readContracts(directory: string): { names: string[]; files: Record<string, string> } {
23
+ const { root, physical, virtual, isVirtual } = clientUiContractPaths(realpathSync(directory));
24
+ const filename = `${root}/apps/web/src/actor-programs/client-ui/contracts.d.ts`;
7
25
  const options: ts.CompilerOptions = {
8
26
  target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext,
9
27
  moduleResolution: ts.ModuleResolutionKind.Bundler, jsx: ts.JsxEmit.ReactJSX,
10
28
  strict: true, skipLibCheck: true, esModuleInterop: true,
11
29
  declaration: true, emitDeclarationOnly: true,
12
30
  };
13
- const program = ts.createProgram([filename], options);
31
+ // TypeScript must see public sources outside node_modules, including during declaration naming.
32
+ const host = ts.createCompilerHost(options);
33
+ host.readFile = (name) => ts.sys.readFile(physical(name));
34
+ host.fileExists = (name) => ts.sys.fileExists(physical(name));
35
+ host.directoryExists = (name) => ts.sys.directoryExists(physical(name));
36
+ host.getDirectories = (name) => ts.sys.getDirectories(physical(name));
37
+ host.realpath = (name) => virtual(ts.sys.realpath!(physical(name)));
38
+ host.getSourceFile = (name, languageVersion) => {
39
+ const text = host.readFile(name);
40
+ return text === undefined ? undefined : ts.createSourceFile(virtual(name), text, languageVersion, true);
41
+ };
42
+ const resolveModule = (name: string, containingFile: string) => {
43
+ const mapped = ts.resolveModuleName(name, containingFile, options, host).resolvedModule;
44
+ if (mapped) return mapped;
45
+ const resolved = ts.resolveModuleName(name, physical(containingFile), options, ts.sys).resolvedModule;
46
+ if (!resolved) return undefined;
47
+ const resolvedFileName = virtual(resolved.resolvedFileName);
48
+ return { ...resolved, resolvedFileName, isExternalLibraryImport: isVirtual(resolvedFileName)
49
+ ? path.posix.relative(root, resolvedFileName).split("/").includes("node_modules") : resolved.isExternalLibraryImport };
50
+ };
51
+ host.resolveModuleNames = (names, containingFile) => names.map((name) => resolveModule(name, containingFile));
52
+ const program = ts.createProgram([filename], options, host);
14
53
  const source = program.getSourceFile(filename);
15
54
  if (!source) throw new Error("The public UI contract is missing.");
16
55
  const checker = program.getTypeChecker();
@@ -27,8 +66,8 @@ function readContracts(repoRoot: string): { names: string[]; files: Record<strin
27
66
  const collect = (file: ts.SourceFile) => {
28
67
  if (visited.has(file.fileName)) return;
29
68
  visited.add(file.fileName);
30
- const relative = path.relative(repoRoot, file.fileName).replaceAll(path.sep, "/");
31
- if (relative.startsWith("../") || relative.includes("node_modules/")) throw new Error("A local UI type reference leaves the public repository sources.");
69
+ const relative = path.posix.relative(root, file.fileName);
70
+ if (!isVirtual(file.fileName) || relative.includes("node_modules/")) throw new Error("A local UI type reference leaves the public repository sources.");
32
71
  diagnostics.push(...program.getSyntacticDiagnostics(file), ...program.getSemanticDiagnostics(file));
33
72
  let text = file.isDeclarationFile ? file.text : "";
34
73
  if (!file.isDeclarationFile) {
@@ -41,13 +80,13 @@ function readContracts(repoRoot: string): { names: string[]; files: Record<strin
41
80
  const references = ts.preProcessFile(text, true, true);
42
81
  for (const reference of references.importedFiles) {
43
82
  if (!reference.fileName.startsWith(".")) continue;
44
- const resolved = ts.resolveModuleName(reference.fileName, file.fileName, options, ts.sys).resolvedModule;
83
+ const resolved = resolveModule(reference.fileName, file.fileName);
45
84
  const dependency = resolved && program.getSourceFile(resolved.resolvedFileName);
46
85
  if (!dependency) throw new Error(`Unresolvable local UI type reference: ${relative}: ${reference.fileName}`);
47
86
  collect(dependency);
48
87
  }
49
88
  for (const reference of declaration.referencedFiles) {
50
- const dependency = program.getSourceFile(path.resolve(path.dirname(file.fileName), reference.fileName));
89
+ const dependency = program.getSourceFile(path.posix.resolve(path.posix.dirname(file.fileName), reference.fileName));
51
90
  if (!dependency) throw new Error(`Unresolvable UI type file reference: ${relative}: ${reference.fileName}`);
52
91
  collect(dependency);
53
92
  }
@@ -76,7 +115,7 @@ export function readClientUiComponentContracts(component: string, repoRoot?: str
76
115
  }
77
116
 
78
117
  export function readClientUiExportContracts(component: string, repoRoot?: string): Record<string, string> {
79
- const root = repoRoot ?? fileURLToPath(new URL("../../../../../", import.meta.url));
118
+ const root = realpathSync(repoRoot ?? fileURLToPath(new URL("../../../../../", import.meta.url)));
80
119
  const catalog = contracts(repoRoot);
81
120
  const files = new Map(Object.entries(catalog.files).map(([name, text]) => [path.resolve(root, name), text]));
82
121
  const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, strict: true, skipLibCheck: true };
@@ -47,7 +47,24 @@ export interface ActorProgramDefinition {
47
47
  views: ActorViewDefinition[];
48
48
  }
49
49
  export interface ActorProgramState { version: 1; program: ActorProgramDefinition | null; }
50
- export interface ActorScriptState { version: 1; entryId: string; }
50
+ /** A start that its program received and has not finished yet. */
51
+ export interface ActorScriptOpenStart { readonly count: number; readonly startedBy: string; }
52
+ /** Where the host took a package from: a run script's template or a plugin's shared packages. */
53
+ export type ActorPackageOrigin = { readonly kind: "script"; readonly entryId: string } | { readonly kind: "shared"; readonly pluginId: string };
54
+ /** identity is the packageIdentity of the sources the host installed; a program whose sources differ is no longer this origin's. */
55
+ export interface ActorScriptPackage { readonly origin: ActorPackageOrigin; readonly identity: string; readonly count: number; readonly open: readonly ActorScriptOpenStart[]; }
56
+ /** An input the host queues for a program to recognize, keyed by the command that queues it: a start, or the result of a start for its starter. */
57
+ export type ActorScriptDelivery = { readonly commandId: string; readonly actorId: string } & (
58
+ | { readonly kind: "start"; readonly name: string; readonly count: number; readonly embedded: boolean; readonly startedBy: string }
59
+ | { readonly kind: "result" });
60
+ /** Who an active program is, for plugins that authorize by it: a shared package, a run script's package, or a package created in the run. */
61
+ export type ActorProgramOrigin = ActorPackageOrigin | { readonly kind: "run"; readonly installedBy: string };
62
+ export interface ActorProgramIdentity { readonly name: string; readonly actorId: string; readonly revision: string; readonly origin: ActorProgramOrigin; }
63
+ export interface ActorScriptState {
64
+ readonly version: 2;
65
+ readonly packages: Readonly<Record<string, ActorScriptPackage>>;
66
+ readonly deliveries: readonly ActorScriptDelivery[];
67
+ }
51
68
 
52
69
  export const resolveActorView = (programs: readonly ActorProgramDefinition[], reference: string) => {
53
70
  const normalized = reference.trim().toLowerCase();
@@ -1,4 +1,5 @@
1
1
  import { serviceToken, type ActorProgramExecutor, type CommandContext } from "@ragents/engine";
2
+ import type { ActorProgramIdentity } from "./contract.js";
2
3
  export interface ActorProgramSource {
3
4
  path: string;
4
5
  content: string;
@@ -9,8 +10,28 @@ export interface ActivatedActorProgram {
9
10
  actorHandle: string;
10
11
  views: number;
11
12
  }
13
+ export interface RunScriptSources {
14
+ entryId: string;
15
+ handle: string;
16
+ files: readonly ActorProgramSource[];
17
+ programs: readonly { name: string; files: readonly ActorProgramSource[] }[];
18
+ sharedPrograms: readonly string[];
19
+ }
20
+ export interface RunScriptStartInput {
21
+ content: string;
22
+ embedded: boolean;
23
+ startedBy: string;
24
+ }
12
25
  export interface ActorProgramsService extends ActorProgramExecutor {
13
26
  workspaceDirectory(runId: string): Promise<string>;
14
- importPackage(context: CommandContext, runId: string, name: string, files: readonly ActorProgramSource[], signal?: AbortSignal, scriptEntryId?: string): Promise<ActivatedActorProgram>;
27
+ importPackage(context: CommandContext, runId: string, name: string, files: readonly ActorProgramSource[], signal?: AbortSignal): Promise<ActivatedActorProgram>;
28
+ /** Installs a run script with its bundled programs, or reuses what the same script installed before; a failure leaves the run unchanged. */
29
+ installScript(context: CommandContext, runId: string, script: RunScriptSources, signal?: AbortSignal): Promise<ActivatedActorProgram>;
30
+ /** Queues a start input for the installed script so that its program recognizes it as a start; an embedded start also places its main view. */
31
+ enqueueStart(context: CommandContext, runId: string, handle: string, start: RunScriptStartInput): { count: number };
32
+ /** Whether a run script installed this TypeScript actor's package in the run. */
33
+ isScriptActor(runId: string, actorId: string): boolean;
34
+ /** Which package the actor runs and where it came from; a package changed in the run counts as created in the run. Authorize by this, never by handle. */
35
+ programOf(runId: string, actorId: string): ActorProgramIdentity | undefined;
15
36
  }
16
37
  export const actorProgramsToken = serviceToken<ActorProgramsService>("ragents.actor-programs.runtime");
@@ -1,6 +1,7 @@
1
1
  import { statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import type {
4
+ ActorPackageContribution,
4
5
  PluginRegistration,
5
6
  RAgentsPlugin,
6
7
  SkillContribution,
@@ -8,7 +9,7 @@ import type {
8
9
  } from "@ragents/engine";
9
10
  import { skillsFromDirectory, type FolderSkills } from "./skills.js";
10
11
  import { pluginFolder } from "./plugins-root.js";
11
- import { runScriptsFromDirectory, type ScriptStartEntry } from "./run-scripts.js";
12
+ import { actorPackagesFromDirectory, runScriptsFromDirectory, type ScriptStartEntry } from "./run-scripts.js";
12
13
  import { systemPromptOptionsFromDirectory, type SystemPromptOption } from "./system-prompts.js";
13
14
 
14
15
  export { pluginFolder };
@@ -37,6 +38,11 @@ export const folderRunScripts = (
37
38
  return directory ? runScriptsFromDirectory(directory, pluginId) : [];
38
39
  };
39
40
 
41
+ export const folderActorPackages = (folder: string): readonly ActorPackageContribution[] => {
42
+ const directory = assetFolder(folder, "actors");
43
+ return directory ? actorPackagesFromDirectory(directory) : [];
44
+ };
45
+
40
46
  export const folderSkillPaths = (folder: string): readonly string[] => folderSkills(folder, path.basename(folder)).paths;
41
47
 
42
48
  export const folderSystemPrompts = (folder: string): readonly SystemPromptOption[] => {
@@ -48,6 +54,7 @@ export const withFolderAssets = (plugin: RAgentsPlugin): RAgentsPlugin => ({
48
54
  manifest: plugin.manifest,
49
55
  register: (host) => {
50
56
  const explicitEntries: StartEntryContribution[] = [];
57
+ const explicitPackages: ActorPackageContribution[] = [];
51
58
  const explicitSkills: SkillContribution[] = [];
52
59
  const observed: PluginRegistration = {
53
60
  ...host,
@@ -55,6 +62,10 @@ export const withFolderAssets = (plugin: RAgentsPlugin): RAgentsPlugin => ({
55
62
  explicitEntries.push(...contributions);
56
63
  host.startEntries(...contributions);
57
64
  },
65
+ actorPackages: (...contributions) => {
66
+ explicitPackages.push(...contributions);
67
+ host.actorPackages(...contributions);
68
+ },
58
69
  skills: (...contributions) => {
59
70
  explicitSkills.push(...contributions);
60
71
  host.skills(...contributions);
@@ -69,6 +80,8 @@ export const withFolderAssets = (plugin: RAgentsPlugin): RAgentsPlugin => ({
69
80
  ...folderRunScripts(folder, plugin.manifest.id),
70
81
  ].filter((entry) => !explicitEntries.some((explicit) => explicit.id === entry.id));
71
82
  if (entries.length > 0) host.startEntries(...entries);
83
+ const packages = folderActorPackages(folder).filter((entry) => !explicitPackages.some((explicit) => explicit.name === entry.name));
84
+ if (packages.length > 0) host.actorPackages(...packages);
72
85
 
73
86
  const skillPaths = skills.paths;
74
87
  if (skillPaths.length === 0) return;
@@ -1,12 +1,12 @@
1
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
- import type { ActorProgramFile, JsonValue, StartEntryContribution } from "@ragents/engine";
3
+ import type { ActorPackageContribution, ActorProgramFile, JsonValue, StartEntryContribution } from "@ragents/engine";
4
4
  import { flag, frontMatterOf, numberOf, tagsOf } from "./front-matter.js";
5
5
 
6
6
  export type ScriptStartEntry = Extract<StartEntryContribution, { action: "script" }>;
7
7
 
8
8
  const PACKAGE_NAME = /^[a-z0-9][a-z0-9-]*$/;
9
- const RUN_FIELDS = ["title", "description", "order", "guide", "tags", "coordinator", "category", "fixed-start-options"];
9
+ const RUN_FIELDS = ["title", "description", "order", "guide", "tags", "coordinator", "embeddable", "shared-programs", "category", "fixed-start-options"];
10
10
 
11
11
  const requiredFile = (directory: string, name: string): string => {
12
12
  const file = path.join(directory, name);
@@ -14,6 +14,16 @@ const requiredFile = (directory: string, name: string): string => {
14
14
  return file;
15
15
  };
16
16
 
17
+ /** The header line shared-programs names shared actor packages of the profile, separated by commas. */
18
+ const sharedProgramsOf = (file: string, value: string | undefined): readonly string[] | undefined => {
19
+ if (value === undefined) return undefined;
20
+ const names = value.split(",").map((name) => name.trim());
21
+ if (names.some((name) => !PACKAGE_NAME.test(name)) || new Set(names).size !== names.length) {
22
+ throw new Error(`${file}: shared-programs must name each shared actor package once, separated by commas`);
23
+ }
24
+ return names;
25
+ };
26
+
17
27
  /** The header line fixed-start-options is a JSON object on one line: start option id to fixed value. */
18
28
  const fixedStartOptionsOf = (file: string, value: string | undefined): Readonly<Record<string, JsonValue>> | undefined => {
19
29
  if (value === undefined) return undefined;
@@ -60,6 +70,7 @@ const runScriptFrom = (directory: string, name: string, id: string): ScriptStart
60
70
  const category = fields.get("category");
61
71
  if (category !== undefined && !category.trim()) throw new Error(`${runFile}: category is empty`);
62
72
  const fixedStartOptions = fixedStartOptionsOf(runFile, fields.get("fixed-start-options"));
73
+ const sharedPrograms = sharedProgramsOf(runFile, fields.get("shared-programs"));
63
74
  const programsDirectory = path.join(directory, "actors");
64
75
  const programsStats = statSync(programsDirectory, {throwIfNoEntry: false});
65
76
  if (programsStats && !programsStats.isDirectory()) throw new Error(`${programsDirectory} must be a directory`);
@@ -80,6 +91,8 @@ const runScriptFrom = (directory: string, name: string, id: string): ScriptStart
80
91
  script: {
81
92
  handle: name,
82
93
  coordinator: flag(runFile, "coordinator", fields.get("coordinator"), true),
94
+ embeddable: flag(runFile, "embeddable", fields.get("embeddable")),
95
+ ...(sharedPrograms !== undefined ? { sharedPrograms } : {}),
83
96
  files: filesOf(directory),
84
97
  programs,
85
98
  },
@@ -98,6 +111,18 @@ export const runScriptsFromDirectory = (directory: string, idPrefix: string): re
98
111
  return packages.map((name) => runScriptFrom(path.join(resolved, name), name, `${idPrefix}.${name}`));
99
112
  };
100
113
 
114
+ /** Every folder is one shared actor package, named after the folder. */
115
+ export const actorPackagesFromDirectory = (directory: string): readonly ActorPackageContribution[] => {
116
+ const resolved = path.resolve(directory);
117
+ const entries = readdirSync(resolved, {withFileTypes: true}).sort((left, right) => left.name.localeCompare(right.name));
118
+ const stray = entries.filter((entry) => !entry.isDirectory()).map((entry) => entry.name);
119
+ if (stray.length > 0) throw new Error(`${resolved} contains files instead of actor package folders: ${stray.join(", ")}`);
120
+ return entries.map((entry) => {
121
+ if (!PACKAGE_NAME.test(entry.name)) throw new Error(`${resolved}: the folder name ${entry.name} is not a package name (lowercase letters, digits, hyphen)`);
122
+ return {name: entry.name, files: filesOf(path.join(resolved, entry.name))};
123
+ });
124
+ };
125
+
101
126
  export const runScriptFromDirectory = (directory: string): ScriptStartEntry => {
102
127
  if (!path.isAbsolute(directory)) throw new Error("The run script package needs an absolute server file path");
103
128
  const resolved = path.resolve(directory);
@@ -6,6 +6,7 @@ import { aliasCompletionModel, aliasCompletionModelToken } from "../plugin-suppo
6
6
  import { withFolderAssets } from "../plugin-support/plugin-folder.js";
7
7
  import type { PluginModule } from "../plugin-support/plugin-module.js";
8
8
  import { runManagementToken } from "../ragents/global-chat.js";
9
+ import { registerRunScriptFunctions } from "../ragents/run-script-tools.js";
9
10
  import { registerTypeScriptFunctions } from "../ragents/typescript-tools.js";
10
11
  import {
11
12
  executorContributionsToken,
@@ -84,6 +85,7 @@ export const composeProfile = (options: ProfileComposition, bridges: HostBridges
84
85
  }));
85
86
  }
86
87
  registerTypeScriptFunctions(host);
88
+ registerRunScriptFunctions(host);
87
89
  host.seal();
88
90
  return host;
89
91
  };