@schlenkr/ragents 0.1.13 → 0.1.15

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 (145) 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 +61 -17
  9. package/apps/server/src/plugin-support/actor-programs/contract.ts +18 -1
  10. package/apps/server/src/plugin-support/actor-programs/service.ts +22 -1
  11. package/apps/server/src/plugin-support/plugin-folder.ts +14 -1
  12. package/apps/server/src/plugin-support/run-scripts.ts +27 -2
  13. package/apps/server/src/profile/compose.ts +2 -0
  14. package/apps/server/src/provider.ts +31 -6
  15. package/apps/server/src/ragents/chat-projection.ts +7 -0
  16. package/apps/server/src/ragents/global-chat.ts +5 -1
  17. package/apps/server/src/ragents/run-script-tools.ts +66 -0
  18. package/apps/server/src/ragents/session.ts +122 -60
  19. package/apps/server/src/title-compactor.ts +1 -1
  20. package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-CpPi6QLr.js → highlighted-body-KPVGNVTW-BwD4uYEe.js} +1 -1
  21. package/apps/web/dist/assets/main-KBrc5Q77.js +11 -0
  22. package/apps/web/dist/assets/{mermaid-HWGCJPDP-q-mf1kLN.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 +16 -2
  34. package/apps/web/dist/host-web.json +13 -13
  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/App.tsx +12 -1
  39. package/apps/web/src/PluginRegistry.tsx +2 -2
  40. package/apps/web/src/SessionList.tsx +7 -6
  41. package/apps/web/src/SettingsModal.tsx +3 -3
  42. package/apps/web/src/actor-programs/client-ui/FilePicker.tsx +1 -1
  43. package/apps/web/src/api.ts +2 -0
  44. package/apps/web/src/run-overview.ts +1 -1
  45. package/apps/web/src/run-panel/RunPanelApp.tsx +1 -1
  46. package/apps/web/src/run-panel/RunPanelMenu.tsx +47 -11
  47. package/apps/web/src/ui/relative-time.ts +1 -1
  48. package/bundles/ragents.activity/ragents-bundle.json +1 -1
  49. package/bundles/ragents.actor-programs/ragents-bundle.json +4 -2
  50. package/bundles/ragents.actor-programs/server/index.js +437 -24
  51. package/bundles/ragents.actor-programs/server/index.js.map +4 -4
  52. package/bundles/ragents.actor-programs/web/index.js.map +1 -1
  53. package/bundles/ragents.ask/ragents-bundle.json +2 -2
  54. package/bundles/ragents.ask/server/index.js +1 -0
  55. package/bundles/ragents.ask/server/index.js.map +2 -2
  56. package/bundles/ragents.browser/ragents-bundle.json +1 -1
  57. package/bundles/ragents.documents/ragents-bundle.json +2 -2
  58. package/bundles/ragents.documents/web/classes.json +1 -1
  59. package/bundles/ragents.documents/web/index.js +1 -1
  60. package/bundles/ragents.documents/web/index.js.map +2 -2
  61. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
  62. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
  63. package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
  64. package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
  65. package/bundles/ragents.orchestration/ragents-bundle.json +7 -4
  66. package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js +11 -0
  67. package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js.map +7 -0
  68. package/bundles/ragents.orchestration/server/exports/server/contract.js +9 -0
  69. package/bundles/ragents.orchestration/server/exports/server/contract.js.map +7 -0
  70. package/bundles/ragents.orchestration/server/index.js +49 -3
  71. package/bundles/ragents.orchestration/server/index.js.map +2 -2
  72. package/bundles/ragents.orchestration/surface.hbs +5 -0
  73. package/bundles/ragents.orchestration/web/index.js +3 -3
  74. package/bundles/ragents.orchestration/web/index.js.map +2 -2
  75. package/bundles/ragents.overseer/ragents-bundle.json +4 -3
  76. package/bundles/ragents.overseer/web/classes.json +1 -1
  77. package/bundles/ragents.overseer/web/index.js +1 -1
  78. package/bundles/ragents.overseer/web/index.js.map +4 -4
  79. package/bundles/ragents.processes/ragents-bundle.json +2 -2
  80. package/bundles/ragents.processes/web/classes.json +1 -1
  81. package/bundles/ragents.processes/web/index.js +1 -1
  82. package/bundles/ragents.processes/web/index.js.map +2 -2
  83. package/bundles/ragents.product/ragents-bundle.json +1 -1
  84. package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
  85. package/bundles/ragents.reference/actors/notebook/package.json +16 -0
  86. package/bundles/ragents.reference/actors/notebook/src/client.tsx +31 -0
  87. package/bundles/ragents.reference/actors/notebook/src/server.ts +27 -0
  88. package/bundles/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
  89. package/bundles/ragents.reference/ragents-bundle.json +4 -3
  90. package/bundles/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
  91. package/bundles/ragents.reference/run-scripts/quick-note/package.json +9 -0
  92. package/bundles/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
  93. package/bundles/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
  94. package/bundles/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
  95. package/bundles/ragents.reference/run-scripts/run-roster/package.json +9 -0
  96. package/bundles/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
  97. package/bundles/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
  98. package/bundles/ragents.todo/ragents-bundle.json +1 -1
  99. package/bundles/ragents.transcript/ragents-bundle.json +1 -1
  100. package/bundles/ragents.watch/ragents-bundle.json +1 -1
  101. package/bundles/ragents.workspace/ragents-bundle.json +2 -2
  102. package/bundles/ragents.workspace/web/classes.json +1 -1
  103. package/bundles/ragents.workspace/web/index.js +1 -1
  104. package/bundles/ragents.workspace/web/index.js.map +2 -2
  105. package/package.json +3 -3
  106. package/packages/ragents/src/domain/vocabulary.ts +4 -0
  107. package/packages/ragents/src/index.ts +3 -1
  108. package/packages/ragents/src/plugin-host.ts +62 -10
  109. package/packages/ragents/src/plugin-types.ts +11 -0
  110. package/packages/ragents/src/runtime/guards.ts +2 -1
  111. package/packages/ragents/src/typescript/async-compiler.ts +11 -1
  112. package/plugins/ragents.actor-programs/server/index.ts +3 -0
  113. package/plugins/ragents.actor-programs/server/runtime.ts +424 -28
  114. package/plugins/ragents.actor-programs/server/script-state.ts +54 -0
  115. package/plugins/ragents.actor-programs/server/tool-contributor.ts +2 -0
  116. package/plugins/ragents.ask/server/ask-tool.ts +1 -0
  117. package/plugins/ragents.documents/web/DocumentViewer.tsx +2 -2
  118. package/plugins/ragents.orchestration/ragents-plugin.json +1 -0
  119. package/plugins/ragents.orchestration/server/contract.ts +14 -0
  120. package/plugins/ragents.orchestration/server/index.ts +4 -1
  121. package/plugins/ragents.orchestration/server/surface-tool.ts +58 -2
  122. package/plugins/ragents.orchestration/surface.hbs +5 -0
  123. package/plugins/ragents.orchestration/web/ExecutionsPanel.tsx +1 -1
  124. package/plugins/ragents.orchestration/web/FlowInspector.tsx +1 -1
  125. package/plugins/ragents.orchestration/web/JournalStatus.tsx +1 -1
  126. package/plugins/ragents.orchestration/web/format.ts +1 -1
  127. package/plugins/ragents.overseer/web/index.tsx +51 -3
  128. package/plugins/ragents.processes/web/processes.ts +1 -1
  129. package/plugins/ragents.reference/actors/notebook/package.json +16 -0
  130. package/plugins/ragents.reference/actors/notebook/src/client.tsx +31 -0
  131. package/plugins/ragents.reference/actors/notebook/src/server.ts +27 -0
  132. package/plugins/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
  133. package/plugins/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
  134. package/plugins/ragents.reference/run-scripts/quick-note/package.json +9 -0
  135. package/plugins/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
  136. package/plugins/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
  137. package/plugins/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
  138. package/plugins/ragents.reference/run-scripts/run-roster/package.json +9 -0
  139. package/plugins/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
  140. package/plugins/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
  141. package/plugins/ragents.workspace/web/FileBrowser.tsx +1 -1
  142. package/scripts/agent/agent-cli.ts +46 -2
  143. package/scripts/package/ragents.mjs +3 -1
  144. package/apps/web/dist/assets/main-DJmCHGzM.js +0 -11
  145. package/apps/web/dist/assets/runPanel-nk_aMxSn.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)$/;
@@ -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
  });
@@ -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
  };
@@ -4,7 +4,7 @@ import { existsSync, statSync } from "node:fs";
4
4
  import { chmod, mkdir, open, readFile, readdir, realpath, rename, rm } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import type { ChatSessionLike, ChatSessionProvider, RunListScope, SessionInfo } from "./chat-handler.js";
7
- import { DomainError, isRunId, unrestrictedAccess, type AccessContext, type HttpRouteContribution, type Journal, type JournalLoadFailure, type MethodContribution, type PluginHost, type ServiceToken, type SessionStartedContext } from "@ragents/engine";
7
+ import { canStartEntry, createAccessContext, DomainError, isRunId, unrestrictedAccess, type AccessContext, type HttpRouteContribution, type Journal, type JournalLoadFailure, type MethodContribution, type PluginHost, type ServiceToken, type SessionStartedContext } from "@ragents/engine";
8
8
  import { WORKSPACE_EXECUTOR_VERSION } from "@ragents/workspace-executor";
9
9
  import { assertRunRights, assertRunWorkspaceAccess, runIdInPath, runListScope, runReachable, type GlobalRunPolicy, type RunAccessPolicy } from "./api/rights.js";
10
10
  import { configuredAnonymousUser, configuredUsers } from "./config-file.js";
@@ -42,7 +42,7 @@ import { RunChatSession } from "./ragents/session.js";
42
42
  import { globalChatToken, globalRunPolicyOf, type GlobalChatPolicy, type ManagedRunStart, type RunManagement } from "./ragents/global-chat.js";
43
43
  import { workspaceRuntimeToken, type SessionWorkspace, type WorkspaceTransfer } from "./ragents/workspace-runtime.js";
44
44
  import { settingsResponse, skillDetailResponse, type SettingsResponse, type SettingsSkillDetail } from "./settings.js";
45
- import { createTitleCompactor } from "./title-compactor.js";
45
+ import { createTitleCompactor, MAX_TITLE_CHARS } from "./title-compactor.js";
46
46
  import { ModelRuntime } from "@ragents/agent";
47
47
  import { TitleSettingsStore } from "./title-settings.js";
48
48
  import { sandboxServicesToken } from "./plugin-support/workspace-sandbox-host.js";
@@ -59,6 +59,8 @@ type ListedSession = SessionInfo & {
59
59
  running: boolean;
60
60
  /** Whether the caller reaches the run's workspace; otherwise UIs hide what needs it. */
61
61
  workspaceAccessible: boolean;
62
+ /** Display name of the user who created the run; missing for a run created without sign-in. */
63
+ ownerLabel?: string;
62
64
  metadata?: Readonly<Record<string, unknown>>;
63
65
  metadataUnavailable?: Readonly<Record<string, string>>;
64
66
  };
@@ -246,9 +248,9 @@ export class RunSessionProvider implements ChatSessionProvider {
246
248
  scriptEntryFor: (entryId) => globalPolicy ? undefined : this.plugins.startEntries.scriptPackage(entryId),
247
249
  startEntryFor: (entryId) => globalPolicy ? undefined : this.plugins.startEntries.entry(entryId),
248
250
  actorPrograms: {
249
- runInput: (...args) => this.plugins.service(actorProgramsToken).runInput(...args),
250
- workspaceDirectory: (runId) => this.plugins.service(actorProgramsToken).workspaceDirectory(runId),
251
- importPackage: (...args) => this.plugins.service(actorProgramsToken).importPackage(...args),
251
+ installScript: (...args) => this.plugins.service(actorProgramsToken).installScript(...args),
252
+ enqueueStart: (...args) => this.plugins.service(actorProgramsToken).enqueueStart(...args),
253
+ isScriptActor: (runId, actorId) => this.plugins.optionalService(actorProgramsToken)?.isScriptActor(runId, actorId) ?? false,
252
254
  },
253
255
  });
254
256
  this.sessions.set(id, session);
@@ -295,6 +297,8 @@ export class RunSessionProvider implements ChatSessionProvider {
295
297
  const isCoordinator = (runId: string) => this.plugins.optionalService(globalChatToken)?.isCoordinator(runId) ?? false;
296
298
  const listed = engine.journal.runIds().filter((runId) => !isCoordinator(runId)
297
299
  && !this.deleteRequested.has(runId) && !this.deleted.has(runId) && visible(runId));
300
+ const anonymousUser = configuredAnonymousUser();
301
+ const userLabels = new Map([...configuredUsers() ?? [], ...anonymousUser ? [anonymousUser] : []].map((user) => [user.id, user.label]));
298
302
  const described = await Promise.all(listed.map(async (runId): Promise<ListedSession | undefined> => {
299
303
  const state = engine.journal.stateOf(runId);
300
304
  if (!state) return undefined;
@@ -317,6 +321,7 @@ export class RunSessionProvider implements ChatSessionProvider {
317
321
  running: [...state.actors.values()].some((actor) => actor.kind !== "human" && engine.scheduler.isRunning(runId, actor.id))
318
322
  || (this.sessions.get(runId)?.running ?? false),
319
323
  workspaceAccessible: workspace,
324
+ ...(state.ownerUserId !== null ? { ownerLabel: userLabels.get(state.ownerUserId) ?? state.ownerUserId } : {}),
320
325
  metadata: metadata.values,
321
326
  ...(Object.keys(metadata.unavailable).length > 0 ? { metadataUnavailable: metadata.unavailable } : {}),
322
327
  };
@@ -358,9 +363,28 @@ export class RunSessionProvider implements ChatSessionProvider {
358
363
  },
359
364
  stop: (runId) => this.openSession(runId).stop(),
360
365
  resetGlobal: (runId) => this.resetGlobalConversation(runId),
366
+ scripts: (runId) => {
367
+ const access = this.ownerAccess(runId);
368
+ return this.openSession(runId).runScripts(this.plugins.startEntries.describe().filter((entry) => canStartEntry(access, entry.id)), access.user?.id ?? null);
369
+ },
370
+ startScript: (runId, entryId, input, startedBy) => {
371
+ const access = this.ownerAccess(runId);
372
+ if (!canStartEntry(access, entryId)) throw new DomainError("access-denied", `The run script ${entryId} is not enabled for the owner of this run.`, 403);
373
+ return this.openSession(runId).startAndWait(entryId, input, access.user ? { id: access.user.id, label: access.user.label } : undefined, startedBy);
374
+ },
361
375
  };
362
376
  }
363
377
 
378
+ /** The owner's access as a user of this profile; the run's own actors start run scripts with it. */
379
+ private ownerAccess(runId: string): AccessContext {
380
+ this.ensureUsable(runId);
381
+ const users = configuredUsers();
382
+ const ownerUserId = this.requireEngine().journal.stateOf(runId)?.ownerUserId ?? null;
383
+ return createAccessContext(ownerUserId === null
384
+ ? { enabled: users !== undefined, user: users ? null : configuredAnonymousUser() ?? null }
385
+ : { enabled: true, user: users?.find((user) => user.id === ownerUserId) ?? null });
386
+ }
387
+
364
388
  private async createManagedRun(start: ManagedRunStart): Promise<string> {
365
389
  const local = start.kind === "package" ? runScriptFromDirectory(start.directory) : undefined;
366
390
  const id = randomUUID();
@@ -924,5 +948,6 @@ const lockedSession = (failure: JournalLoadFailure, journal: Journal): ListedSes
924
948
  const sessionTitle = (text: string | undefined): string | undefined => {
925
949
  const lines = text?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) ?? [];
926
950
  if (lines.length === 0) return undefined;
927
- return lines[0]?.startsWith("[Test:") && lines[1] ? `${lines[0]} ${lines[1]}` : lines[0];
951
+ const title = lines[0]?.startsWith("[Test:") && lines[1] ? `${lines[0]} ${lines[1]}` : lines[0] ?? "";
952
+ return title.length > MAX_TITLE_CHARS ? `${title.slice(0, MAX_TITLE_CHARS - 3)}...` : title;
928
953
  };
@@ -11,6 +11,8 @@ export interface ChatProjectionScope {
11
11
  primaryActorId: string;
12
12
  ownerId: string;
13
13
  labelOf: (actorId: string) => string | undefined;
14
+ /** The handle of a TypeScript actor a run script installed; the chat shows its runtime output too. */
15
+ scriptActorHandle?: (actorId: string) => string | undefined;
14
16
  turnOf: (turnId: string) => Pick<Turn, "actorId" | "toolCalls">;
15
17
  /** Whether this command also interrupted a turn of the actor; its stop then repeats no reason. */
16
18
  interruptedByCommand: (commandId: string, actorId: string) => boolean;
@@ -51,6 +53,11 @@ export const chatEventsOf = (event: JournalEvent, scope: ChatProjectionScope, cu
51
53
  if (event.type === "turn.interrupted")
52
54
  return scope.turnOf(event.payload.turnId).actorId === scope.primaryActorId ? journalChatEventsOf(event, cursor, interruptedIn(scope)) : [];
53
55
 
56
+ if (event.type === "runtime.output.recorded" && event.actorId !== scope.primaryActorId) {
57
+ const handle = scope.scriptActorHandle?.(event.actorId);
58
+ return handle ? [{ kind: "system", text: `@${handle}: ${event.payload.text}`, at }] : [];
59
+ }
60
+
54
61
  if (event.actorId !== scope.primaryActorId) return [];
55
62
 
56
63
  if (event.type === "plugin.state-replaced" || event.type === "plugin.state-patched") return [{
@@ -1,6 +1,6 @@
1
1
  import { serviceToken, type AccessContext, type CatalogModel, type ExecutableActor, type JournalEvent, type JsonValue, type ModelSelection, type Orchestration, type RunView } from "@ragents/engine";
2
2
  import type { GlobalRunPolicy } from "../api/rights.js";
3
- import type { ChatUser, SessionInfo } from "../chat-handler.js";
3
+ import type { ChatUser, RunScriptListing, SessionInfo, StartedScript } from "../chat-handler.js";
4
4
 
5
5
  export interface GlobalChatPolicy {
6
6
  /** A user's coordinator; without sign-in (null) there is exactly one. */
@@ -49,6 +49,10 @@ export interface RunManagement {
49
49
  send: (runId: string, message: string, access: AccessContext) => Promise<void>;
50
50
  stop: (runId: string) => Promise<void>;
51
51
  resetGlobal: (runId: string) => Promise<void>;
52
+ /** The run scripts the run's owner may start, and whether each can start in the run now. */
53
+ scripts: (runId: string) => readonly RunScriptListing[];
54
+ /** Starts a run script in the run as its owner would; startedBy is the actor that receives its result. */
55
+ startScript: (runId: string, entryId: string, input: JsonValue | null, startedBy: string) => Promise<StartedScript>;
52
56
  }
53
57
 
54
58
  export const globalChatToken = serviceToken<GlobalChatPolicy>("host.global-chat");