@schlenkr/ragents 0.1.14 → 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 (113) 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 +23 -4
  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/web/dist/assets/{highlighted-body-KPVGNVTW-BslIwn_y.js → highlighted-body-KPVGNVTW-BwD4uYEe.js} +1 -1
  20. package/apps/web/dist/assets/{main-BUN8m8SW.js → main-KBrc5Q77.js} +1 -1
  21. package/apps/web/dist/assets/{mermaid-HWGCJPDP-B1erZlFM.js → mermaid-HWGCJPDP-CSCkaArr.js} +88 -88
  22. package/apps/web/dist/assets/runPanel-BjG5NBKm.js +26 -0
  23. package/apps/web/dist/help/guide-clients.html +21 -4
  24. package/apps/web/dist/help/guide-clients.md +23 -4
  25. package/apps/web/dist/help/guide-plugins.html +12 -2
  26. package/apps/web/dist/help/guide-plugins.md +9 -2
  27. package/apps/web/dist/help/guide-programs.html +8 -2
  28. package/apps/web/dist/help/guide-programs.md +9 -2
  29. package/apps/web/dist/help/guide-runtime.html +3 -2
  30. package/apps/web/dist/help/guide-runtime.md +3 -2
  31. package/apps/web/dist/help/index.html +1 -1
  32. package/apps/web/dist/help/mini-app.css +7 -2
  33. package/apps/web/dist/host-web.json +6 -6
  34. package/apps/web/dist/index.html +2 -2
  35. package/apps/web/dist/run-panel.html +2 -2
  36. package/apps/web/package.json +1 -1
  37. package/apps/web/src/run-panel/RunPanelApp.tsx +1 -1
  38. package/apps/web/src/run-panel/RunPanelMenu.tsx +47 -11
  39. package/bundles/ragents.activity/ragents-bundle.json +1 -1
  40. package/bundles/ragents.actor-programs/ragents-bundle.json +4 -2
  41. package/bundles/ragents.actor-programs/server/index.js +437 -24
  42. package/bundles/ragents.actor-programs/server/index.js.map +4 -4
  43. package/bundles/ragents.actor-programs/web/index.js.map +1 -1
  44. package/bundles/ragents.ask/ragents-bundle.json +2 -2
  45. package/bundles/ragents.ask/server/index.js +1 -0
  46. package/bundles/ragents.ask/server/index.js.map +2 -2
  47. package/bundles/ragents.browser/ragents-bundle.json +1 -1
  48. package/bundles/ragents.documents/ragents-bundle.json +1 -1
  49. package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
  50. package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
  51. package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
  52. package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
  53. package/bundles/ragents.orchestration/ragents-bundle.json +7 -4
  54. package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js +11 -0
  55. package/bundles/ragents.orchestration/server/chunks/chunk-QS3WJ3D7.js.map +7 -0
  56. package/bundles/ragents.orchestration/server/exports/server/contract.js +9 -0
  57. package/bundles/ragents.orchestration/server/exports/server/contract.js.map +7 -0
  58. package/bundles/ragents.orchestration/server/index.js +49 -3
  59. package/bundles/ragents.orchestration/server/index.js.map +2 -2
  60. package/bundles/ragents.orchestration/surface.hbs +5 -0
  61. package/bundles/ragents.overseer/ragents-bundle.json +1 -1
  62. package/bundles/ragents.processes/ragents-bundle.json +1 -1
  63. package/bundles/ragents.product/ragents-bundle.json +1 -1
  64. package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
  65. package/bundles/ragents.reference/actors/notebook/package.json +16 -0
  66. package/bundles/ragents.reference/actors/notebook/src/client.tsx +31 -0
  67. package/bundles/ragents.reference/actors/notebook/src/server.ts +27 -0
  68. package/bundles/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
  69. package/bundles/ragents.reference/ragents-bundle.json +4 -3
  70. package/bundles/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
  71. package/bundles/ragents.reference/run-scripts/quick-note/package.json +9 -0
  72. package/bundles/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
  73. package/bundles/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
  74. package/bundles/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
  75. package/bundles/ragents.reference/run-scripts/run-roster/package.json +9 -0
  76. package/bundles/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
  77. package/bundles/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
  78. package/bundles/ragents.todo/ragents-bundle.json +1 -1
  79. package/bundles/ragents.transcript/ragents-bundle.json +1 -1
  80. package/bundles/ragents.watch/ragents-bundle.json +1 -1
  81. package/bundles/ragents.workspace/ragents-bundle.json +1 -1
  82. package/package.json +3 -3
  83. package/packages/ragents/src/domain/vocabulary.ts +4 -0
  84. package/packages/ragents/src/index.ts +3 -1
  85. package/packages/ragents/src/plugin-host.ts +62 -10
  86. package/packages/ragents/src/plugin-types.ts +11 -0
  87. package/packages/ragents/src/runtime/guards.ts +2 -1
  88. package/packages/ragents/src/typescript/async-compiler.ts +11 -1
  89. package/plugins/ragents.actor-programs/server/index.ts +3 -0
  90. package/plugins/ragents.actor-programs/server/runtime.ts +424 -28
  91. package/plugins/ragents.actor-programs/server/script-state.ts +54 -0
  92. package/plugins/ragents.actor-programs/server/tool-contributor.ts +2 -0
  93. package/plugins/ragents.ask/server/ask-tool.ts +1 -0
  94. package/plugins/ragents.orchestration/ragents-plugin.json +1 -0
  95. package/plugins/ragents.orchestration/server/contract.ts +14 -0
  96. package/plugins/ragents.orchestration/server/index.ts +4 -1
  97. package/plugins/ragents.orchestration/server/surface-tool.ts +58 -2
  98. package/plugins/ragents.orchestration/surface.hbs +5 -0
  99. package/plugins/ragents.reference/actors/notebook/package.json +16 -0
  100. package/plugins/ragents.reference/actors/notebook/src/client.tsx +31 -0
  101. package/plugins/ragents.reference/actors/notebook/src/server.ts +27 -0
  102. package/plugins/ragents.reference/actors/notebook/tests/program.test.ts +27 -0
  103. package/plugins/ragents.reference/run-scripts/quick-note/RUN.md +14 -0
  104. package/plugins/ragents.reference/run-scripts/quick-note/package.json +9 -0
  105. package/plugins/ragents.reference/run-scripts/quick-note/src/server.ts +29 -0
  106. package/plugins/ragents.reference/run-scripts/quick-note/tests/program.test.ts +43 -0
  107. package/plugins/ragents.reference/run-scripts/run-roster/RUN.md +19 -0
  108. package/plugins/ragents.reference/run-scripts/run-roster/package.json +9 -0
  109. package/plugins/ragents.reference/run-scripts/run-roster/src/server.ts +26 -0
  110. package/plugins/ragents.reference/run-scripts/run-roster/tests/program.test.ts +47 -0
  111. package/scripts/agent/agent-cli.ts +46 -2
  112. package/scripts/package/ragents.mjs +3 -1
  113. package/apps/web/dist/assets/runPanel-s8gGpr9M.js +0 -21
@@ -10,6 +10,7 @@ const metadata = [
10
10
  { name: "actor_program_remove", label: "Remove Actor Program", description: "Detach functions and views and stop the backend.", longDescription: "A TypeScript actor is stopped; an LLM actor retains its agent behavior." },
11
11
  { name: "actor_view_set_visibility", label: "Show or hide Actor View", description: "Set surface visibility by package-name/view-key or @handle/view-key.", longDescription: "Use names you chose; the server resolves the view ID. A unique view title also works." },
12
12
  { name: "actor_program_diagnostics", label: "Actor Program Diagnostics", description: "Read the last project diagnostics.", longDescription: "Changed errors are automatically supplied before the next model request." },
13
+ { name: "actor_program_ensure", label: "Ensure Actor Program", description: "Make a package active once: return it when active, restart its stopped actor, activate it when installed, or install a shared package of the profile.", longDescription: "Safe to call on every start: an active package is returned unchanged and not rebuilt. status names what happened. actor_program_activate instead rebuilds and reactivates." },
13
14
  ] as const;
14
15
  export const actorProgramManagementToolNames = metadata.map((entry) => entry.name);
15
16
  export const actorProgramToolsAvailable = defineToolAvailability({ availability: "conditional", availabilityDetail: "For executable actors with agent.spawn and plugin.state.write." }, actor => actor.kind !== "human" && holdsUsable(actor, "agent.spawn") && holdsUsable(actor, "plugin.state.write"));
@@ -22,6 +23,7 @@ const managementTools = (runtime: ActorProgramRuntime, diagnostics: Diagnostics)
22
23
  defineRunFunction({ ...metadata[3], schema: Type.Object({ name }, { additionalProperties: false }), resultSchema: Type.Object({ removed: Type.String() }, { additionalProperties: false }), available: actorProgramToolsAvailable, executionMode: "sequential", run: (scope, id, input) => runtime.remove(scope.context(id), scope.caller.runId, input.name) }),
23
24
  defineRunFunction({ ...metadata[4], schema: Type.Object({ view: Type.String({ minLength: 1, description: "package-name/view-key or @handle/view-key of an activated view, without the surface entity prefix app:. No generated IDs needed." }), visible: Type.Boolean() }, { additionalProperties: false }), resultSchema: Type.Object({ view: Type.String(), visible: Type.Boolean() }, { additionalProperties: false }), available: actorProgramToolsAvailable, executionMode: "sequential", run: (scope, id, input) => runtime.setVisibility(scope.context(id), scope.caller.runId, input.view, input.visible) }),
24
25
  defineRunFunction({ ...metadata[5], schema: Type.Object({ name: Type.Optional(name) }, { additionalProperties: false }), resultSchema: Type.String(), available: actorProgramToolsAvailable, executionMode: "sequential", run: (scope, _id, input) => diagnostics.latest(scope.caller.runId, scope.caller.actorId, input.name) }),
26
+ defineRunFunction({ ...metadata[6], schema: Type.Object({ name }, { additionalProperties: false }), resultSchema: Type.Object({ actorId: Type.String(), handle: Type.String(), status: Type.Union([Type.Literal("active"), Type.Literal("restarted"), Type.Literal("activated"), Type.Literal("installed")]) }, { additionalProperties: false }), available: actorProgramToolsAvailable, executionMode: "sequential", run: (scope, id, input) => runtime.ensure(scope.context(id), scope.caller.runId, input.name, scope.signal) }),
25
27
  ];
26
28
  export const createActorProgramToolContributors = (runtime: ActorProgramRuntime, diagnostics: Diagnostics): readonly ToolContributor[] => [
27
29
  { name: "ragents.actor-programs.management", descriptors: metadata.map((item) => toolDescriptorFrom(item, actorProgramToolsAvailable)), tools: () => managementTools(runtime, diagnostics) },
@@ -6,6 +6,7 @@ import type { AskService } from "./contract.js";
6
6
  export const askToolMetadata = {
7
7
  name: "ask_user",
8
8
  label: "Question",
9
+ nativeTool: true,
9
10
  description: "Asks the user for a required decision with answer options and waits for the answer.",
10
11
  longDescription: "Always use this tool when you need a decision from the user "
11
12
  + "(e.g. choosing a branch or a time range), instead of only asking the question as text. "
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "ragents.orchestration",
3
3
  "exports": {
4
+ "server": ["server/contract"],
4
5
  "web": ["web/surface-entities", "web/program-slot", "web/tile-docking"]
5
6
  }
6
7
  }
@@ -0,0 +1,14 @@
1
+ import { serviceToken, type CommandContext, type ServiceToken } from "@ragents/engine";
2
+
3
+ export interface SurfacePlacementRequest {
4
+ entity: string;
5
+ direction?: "horizontal" | "vertical";
6
+ weight?: number;
7
+ }
8
+
9
+ /** Places an entity next to a run's surface layout, as canvas_layout_place does; an entity the layout shows already stays. */
10
+ export interface SurfacePlacement {
11
+ place: (context: CommandContext, runId: string, request: SurfacePlacementRequest) => { placed: boolean };
12
+ }
13
+
14
+ export const surfacePlacementToken: ServiceToken<SurfacePlacement> = serviceToken("ragents.orchestration.surface-placement");
@@ -11,7 +11,8 @@ import { runtimeProviderToken } from "@ragents/host/ragents/host-services.js";
11
11
  import { pluginAsset } from "@ragents/host/plugin-support/plugin-folder.js";
12
12
  import type { PluginModule } from "@ragents/host/plugin-support/plugin-module.js";
13
13
  import { boundToTools, handlebarsPrompt } from "@ragents/host/plugin-support/prompt.js";
14
- import { createSurfaceToolContributor } from "./surface-tool.js";
14
+ import { createSurfaceToolContributor, placeOnSurface } from "./surface-tool.js";
15
+ import { surfacePlacementToken } from "./contract.js";
15
16
  import { createRunStopContributor } from "./run-stop-tool.js";
16
17
  import { runManagementToken } from "@ragents/host/ragents/global-chat.js";
17
18
 
@@ -19,6 +20,7 @@ const orchestrationPlugin: RAgentsPlugin = {
19
20
  manifest: { id: "ragents.orchestration" },
20
21
  register: (host) => {
21
22
  host.functions(createSurfaceToolContributor());
23
+ host.provide(surfacePlacementToken, placeOnSurface(host.service(runtimeProviderToken)));
22
24
  host.functions(createRunStopContributor(
23
25
  (runId) => host.service(runManagementToken)().stop(runId),
24
26
  (error) => console.error("Run stop by the coordinator failed", error),
@@ -53,6 +55,7 @@ const orchestrationPlugin: RAgentsPlugin = {
53
55
  boundToTools(
54
56
  handlebarsPrompt("ragents.orchestration.surface", 650, pluginAsset("ragents.orchestration", "surface.hbs")),
55
57
  "canvas_layout_replace",
58
+ "canvas_layout_place",
56
59
  ),
57
60
  );
58
61
  },
@@ -5,6 +5,8 @@ import {
5
5
  defineRunFunction,
6
6
  defineToolAvailability,
7
7
  holdsUsable,
8
+ type CommandContext,
9
+ type Orchestration,
8
10
  type RunFunction,
9
11
  type RunView,
10
12
  type ToolContributor,
@@ -18,6 +20,7 @@ import {
18
20
  type SurfaceLayout,
19
21
  } from "../contract.js";
20
22
  import { surfaceTileEntities, mapSurfaceTileEntities } from "../tiled-layout.js";
23
+ import type { SurfacePlacement, SurfacePlacementRequest } from "./contract.js";
21
24
 
22
25
  export const surfaceToolMetadata = {
23
26
  name: "canvas_layout_replace",
@@ -34,6 +37,17 @@ export const surfaceToolMetadata = {
34
37
  + "Participants you do not place stay reachable through the run header.",
35
38
  } as const;
36
39
 
40
+ export const placeToolMetadata = {
41
+ name: "canvas_layout_place",
42
+ label: "Place on Surface",
43
+ description: "Place one actor or mini-app next to the current surface layout instead of replacing it.",
44
+ longDescription: "entity is {entity} of a tile: '@helper' or 'app:@workspace/main'. The current arrangement becomes one side of a new split "
45
+ + "and the entity the other: direction 'horizontal' (default) puts it to the right, 'vertical' below. weight is its share against "
46
+ + "the existing arrangement's weight 1, default 1. An entity the layout already shows stays where it is; placed is then false. "
47
+ + "On an empty surface the entity becomes the only tile. Use it when you join a running run, such as a run script started with "
48
+ + "start.embedded, so that the tiles others arranged stay.",
49
+ } as const;
50
+
37
51
  export const canDesignSurface = defineToolAvailability({
38
52
  availability: "conditional",
39
53
  availabilityDetail: "Only for agents and TypeScript actors with the capability plugin.state.write.",
@@ -117,6 +131,48 @@ export const checkLayoutAgainstRun = (view: RunView, input: SurfaceLayout): Surf
117
131
  return layout;
118
132
  };
119
133
 
134
+ const currentLayout = (view: RunView): SurfaceLayout => {
135
+ const state = view.pluginStates.find((entry) => entry.pluginId === ORCHESTRATION_PLUGIN_ID && entry.scope.kind === "run")?.state;
136
+ return state === undefined || state === null ? { root: null } : surfaceLayoutOf(state);
137
+ };
138
+
139
+ /** Splits the root once: the existing arrangement keeps weight 1, the placed tile gets its own. */
140
+ const placed = (view: RunView, request: SurfacePlacementRequest): SurfaceLayout | undefined => {
141
+ const current = currentLayout(view);
142
+ const [tile] = surfaceTileEntities(checkLayoutAgainstRun(view, parsedLayout({ root: { entity: request.entity } })).root);
143
+ if (surfaceTileEntities(current.root).includes(tile!)) return undefined;
144
+ const root = current.root === null ? { entity: tile! } : {
145
+ direction: request.direction ?? "horizontal",
146
+ weights: [1, request.weight ?? 1],
147
+ children: [current.root, { entity: tile! }],
148
+ };
149
+ return parsedLayout({ root });
150
+ };
151
+
152
+ export const placeOnSurface = (runtime: () => Orchestration): SurfacePlacement => ({
153
+ place: (context: CommandContext, runId: string, request: SurfacePlacementRequest) => {
154
+ const layout = placed(runtime().view(runId), request);
155
+ if (!layout) return { placed: false };
156
+ runtime().replacePluginState(context, runId, { pluginId: ORCHESTRATION_PLUGIN_ID, scope: { kind: "run" }, state: layout });
157
+ return { placed: true };
158
+ },
159
+ });
160
+
161
+ const placeSchema = Type.Object({
162
+ entity: Type.String({ minLength: 1, description: "Actor @handle or activated mini-app app:@handle/view-key or app:program-name/view-key" }),
163
+ direction: Type.Optional(Type.Union([Type.Literal("horizontal"), Type.Literal("vertical")], { description: "horizontal: to the right of the current layout (default); vertical: below it" })),
164
+ weight: Type.Optional(Type.Number({ exclusiveMinimum: 0, description: "Share of the placed tile against the existing arrangement's weight 1; default 1" })),
165
+ }, { additionalProperties: false });
166
+
167
+ export const createPlaceTool = (): RunFunction =>
168
+ defineRunFunction({
169
+ ...placeToolMetadata,
170
+ schema: placeSchema,
171
+ resultSchema: Type.Object({ placed: Type.Boolean() }, { additionalProperties: false }),
172
+ available: canDesignSurface,
173
+ run: ({ runtime, caller, context }, toolCallId, input) => placeOnSurface(() => runtime).place(context(toolCallId), caller.runId, input),
174
+ });
175
+
120
176
  export const createSurfaceTool = (): RunFunction =>
121
177
  defineRunFunction({
122
178
  ...surfaceToolMetadata,
@@ -137,6 +193,6 @@ export const createSurfaceTool = (): RunFunction =>
137
193
 
138
194
  export const createSurfaceToolContributor = (): ToolContributor => ({
139
195
  name: "ragents.orchestration.surface",
140
- descriptors: [toolDescriptorFrom(surfaceToolMetadata, canDesignSurface)],
141
- tools: () => [createSurfaceTool()],
196
+ descriptors: [toolDescriptorFrom(surfaceToolMetadata, canDesignSurface), toolDescriptorFrom(placeToolMetadata, canDesignSurface)],
197
+ tools: () => [createSurfaceTool(), createPlaceTool()],
142
198
  });
@@ -19,6 +19,11 @@ App left, chat right at 50:50:
19
19
  ]}}. Resolve the actual actor and app names before calling; the example names are not existing
20
20
  participants.
21
21
 
22
+ To add one tile without replacing what is arranged, `context.functions.canvas_layout_place({ entity })`
23
+ splits the current layout once and puts the entity to its right, or below with `direction: "vertical"`;
24
+ `weight` is its share against the existing arrangement's weight 1. An entity the layout shows already
25
+ stays where it is. A run script started inside a running run (`start.embedded`) uses place, not replace.
26
+
22
27
  An actor tile carries {entity:"@helper",chatInput:false} when its chat composer should stay hidden.
23
28
  That changes neither permissions nor the inspector. Mini-app tiles have no chatInput.
24
29
 
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "notebook",
3
+ "private": true,
4
+ "type": "module",
5
+ "ragents": {
6
+ "title": "Notebook",
7
+ "description": "Keeps the notes that run scripts send it, newest first.",
8
+ "backend": "src/server.ts",
9
+ "views": [
10
+ {
11
+ "id": "main",
12
+ "client": "src/client.tsx"
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { context, useAppState } from "@ragents/client";
4
+ import * as UI from "@ragents/client/ui";
5
+
6
+ const App = () => {
7
+ const state = useAppState();
8
+ const notes = state.notes ?? [];
9
+ const [status, setStatus] = React.useState("Run scripts add their notes here.");
10
+
11
+ const clear = async (): Promise<void> => {
12
+ const answer = await context.capabilities.call("clear", {});
13
+ setStatus(`Cleared ${answer.cleared} ${answer.cleared === 1 ? "note" : "notes"}.`);
14
+ };
15
+
16
+ return (
17
+ <UI.AppLayout title="Notebook" description={<>{notes.length} {notes.length === 1 ? "note" : "notes"}</>}>
18
+ <UI.Stack>
19
+ <p className="text-muted-foreground" role="status">{status}</p>
20
+ <ul className="border-t border-border">
21
+ {notes.map((note, index) => <li className="min-h-8 border-b border-border-soft py-1.5 [overflow-wrap:anywhere]" key={index}>{note}</li>)}
22
+ </ul>
23
+ <UI.Button disabled={notes.length === 0} onClick={() => void clear()} variant="outline">Clear</UI.Button>
24
+ </UI.Stack>
25
+ </UI.AppLayout>
26
+ );
27
+ };
28
+
29
+ const root = document.getElementById("root");
30
+ if (!root) throw new Error("The #root element is missing.");
31
+ createRoot(root).render(<App />);
@@ -0,0 +1,27 @@
1
+ import { defineActor } from "@ragents/server";
2
+ import { Type } from "typebox";
3
+
4
+ const KEPT_NOTES = 50;
5
+
6
+ const contract = {
7
+ state: Type.Object({ notes: Type.Optional(Type.Array(Type.String())) }, { additionalProperties: false }),
8
+ functions: {
9
+ clear: { label: "Clear notes", input: Type.Object({}, { additionalProperties: false }), output: Type.Object({ cleared: Type.Integer() }, { additionalProperties: false }) },
10
+ },
11
+ input: { capabilities: [] },
12
+ } as const;
13
+
14
+ export default defineActor(contract, {
15
+ functions: {
16
+ clear: (_input, context) => {
17
+ const cleared = context.state.read().notes?.length ?? 0;
18
+ context.state.replace({ notes: [] });
19
+ return { cleared };
20
+ },
21
+ },
22
+ onInput: (input, context) => {
23
+ const note = input.content.trim();
24
+ if (!note) throw new Error("A note needs text.");
25
+ context.state.replace({ notes: [note, ...context.state.read().notes ?? []].slice(0, KEPT_NOTES) });
26
+ },
27
+ });
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTestContext } from "@ragents/server/testing";
4
+ import program from "../src/server.ts";
5
+
6
+ const input = (content: string) => ({ id: "input-1", content, artifactIds: [], sourceEventIds: [], subscriptionId: null, event: null });
7
+
8
+ test("keeps notes newest first and at most fifty", async () => {
9
+ const context = createTestContext<{ notes?: string[] }>({ state: { notes: Array.from({ length: 50 }, (_, index) => `old ${index}`) } });
10
+ await program.onInput!(input(" new note "), context);
11
+ const notes = context.state.read().notes!;
12
+ assert.equal(notes.length, 50);
13
+ assert.equal(notes[0], "new note");
14
+ assert.equal(notes.at(-1), "old 48");
15
+ });
16
+
17
+ test("an empty note is refused and changes nothing", async () => {
18
+ const context = createTestContext<{ notes?: string[] }>({ state: { notes: ["kept"] } });
19
+ await assert.rejects(async () => program.onInput!(input(" "), context), /needs text/);
20
+ assert.deepEqual(context.state.read(), { notes: ["kept"] });
21
+ });
22
+
23
+ test("clear empties the notebook and names how many notes it removed", async () => {
24
+ const context = createTestContext<{ notes?: string[] }>({ state: { notes: ["one", "two"] } });
25
+ assert.deepEqual(await program.functions.clear({}, context), { cleared: 2 });
26
+ assert.deepEqual(context.state.read(), { notes: [] });
27
+ });
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Add a quick note
3
+ description: "A prepared one-step script shows a shared actor package: it adds a note to the notebook it shares with the roster check, in a new or a running run."
4
+ order: 180
5
+ coordinator: false
6
+ embeddable: true
7
+ shared-programs: notebook
8
+ tags: Run scripts, Concept demo, TypeScript actors
9
+ ---
10
+
11
+ The start value `{ "text": "..." }` is the note; without one, the script notes when it was
12
+ started. The script makes the shared notebook active with `actor_program_ensure`, sends it the
13
+ note, places the notebook's view with `canvas_layout_place`, and ends the start with
14
+ `context.finish`. Whether the roster check or this script comes first, the run has one notebook.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "quick-note",
3
+ "private": true,
4
+ "type": "module",
5
+ "ragents": {
6
+ "title": "Quick note",
7
+ "backend": "src/server.ts"
8
+ }
9
+ }
@@ -0,0 +1,29 @@
1
+ import { defineActor } from "@ragents/server";
2
+ import { Type } from "typebox";
3
+
4
+ const contract = {
5
+ state: Type.Object({}, { additionalProperties: false }),
6
+ functions: {},
7
+ input: { capabilities: ["actor_program_ensure", "actor_input", "canvas_layout_place"] },
8
+ } as const;
9
+
10
+ const noteOf = (input: unknown, now: string): string => {
11
+ if (input === null) return `Started at ${now}.`;
12
+ const text = typeof input === "object" && input !== null && "text" in input ? (input as { text: unknown }).text : undefined;
13
+ if (typeof text !== "string" || !text.trim()) throw new Error('The start value is { "text": "..." } with the note, or none.');
14
+ return text.trim();
15
+ };
16
+
17
+ export default defineActor(contract, {
18
+ functions: {},
19
+ onInput: () => {
20
+ throw new Error("The quick note answers only starts; start it again with the note as its start value.");
21
+ },
22
+ onStart: async (start, context) => {
23
+ const note = noteOf(start.input, context.std.now());
24
+ const notebook = await context.functions.actor_program_ensure({ name: "notebook" });
25
+ await context.functions.actor_input({ actor: `@${notebook.handle}`, content: note });
26
+ await context.functions.canvas_layout_place({ entity: "app:notebook/main" });
27
+ context.finish({ note, notebook: notebook.status }, { summary: `Noted: ${note}` });
28
+ },
29
+ });
@@ -0,0 +1,43 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTestContext } from "@ragents/server/testing";
4
+ import program from "../src/server.ts";
5
+
6
+ const start = (input: unknown) => ({ input, options: {}, embedded: true, startedBy: "owner-id", count: 1 });
7
+
8
+ const note = (status: "active" | "installed" = "installed") => {
9
+ const calls: { name: string; input: unknown }[] = [];
10
+ const record = <T>(name: string, answer: T) => (input: unknown): T => { calls.push({ name, input }); return answer; };
11
+ const context = createTestContext<Record<string, never>>({ state: {}, functions: {
12
+ actor_program_ensure: record("actor_program_ensure", { actorId: "id-notebook", handle: "notebook", status }),
13
+ actor_input: record("actor_input", []),
14
+ canvas_layout_place: record("canvas_layout_place", { placed: status === "installed" }),
15
+ } });
16
+ return { calls, context };
17
+ };
18
+
19
+ test("sends the note to the shared notebook and reports it", async () => {
20
+ const { calls, context } = note();
21
+ await program.onStart!(start({ text: " Ask about the budget " }), context);
22
+ assert.deepEqual(calls.map((call) => [call.name, call.input]), [
23
+ ["actor_program_ensure", { name: "notebook" }],
24
+ ["actor_input", { actor: "@notebook", content: "Ask about the budget" }],
25
+ ["canvas_layout_place", { entity: "app:notebook/main" }],
26
+ ]);
27
+ assert.deepEqual(context.finished, [{ result: { note: "Ask about the budget", notebook: "installed" }, summary: "Noted: Ask about the budget" }]);
28
+ });
29
+
30
+ test("without a start value it notes when it started; an invalid value changes nothing", async () => {
31
+ const { calls, context } = note("active");
32
+ await program.onStart!(start(null), context);
33
+ assert.match(String((calls[1]!.input as { content: string }).content), /^Started at /);
34
+ const invalid = note();
35
+ await assert.rejects(async () => program.onStart!(start({ text: "" }), invalid.context), /start value/);
36
+ assert.deepEqual(invalid.calls, []);
37
+ assert.deepEqual(invalid.context.finished, []);
38
+ });
39
+
40
+ test("an ordinary message is refused", async () => {
41
+ const { context } = note();
42
+ await assert.rejects(async () => program.onInput!({ id: "input-1", content: "hello", artifactIds: [], sourceEventIds: [], subscriptionId: null, event: null }, context), /answers only starts/);
43
+ });
@@ -0,0 +1,19 @@
1
+ ---
2
+ title: Take stock of the run
3
+ description: "A prepared check shows a run script that also joins a running run. It lists the other participants, notes them in the shared notebook, and reports them back as its result."
4
+ order: 170
5
+ coordinator: false
6
+ embeddable: true
7
+ shared-programs: notebook
8
+ tags: Run scripts, Concept demo, TypeScript actors
9
+ ---
10
+
11
+ The TypeScript program reads the participants with `actor_list` and ends each start with
12
+ `context.finish`. The owner reads the summary in the chat; a coordinator that starts it through
13
+ `run_script_start` receives summary and list as a message. Started inside a running run through the
14
+ run menu, `ragents script`, or the coordinator, it changes neither the primary actor nor the tiles,
15
+ and a repeated start reuses the same actor. Started as a new run, it is the run's only participant.
16
+
17
+ It shares the notebook with the quick note script: `shared-programs: notebook` copies the plugin's
18
+ shared package into the run, `actor_program_ensure` makes it active once, and
19
+ `canvas_layout_place` puts its view next to what is arranged.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "run-roster",
3
+ "private": true,
4
+ "type": "module",
5
+ "ragents": {
6
+ "title": "Run roster",
7
+ "backend": "src/server.ts"
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ import { defineActor } from "@ragents/server";
2
+ import { Type } from "typebox";
3
+
4
+ const contract = {
5
+ state: Type.Object({ reports: Type.Optional(Type.Number()) }, { additionalProperties: false }),
6
+ functions: {},
7
+ input: { capabilities: ["actor_list", "actor_program_ensure", "actor_input", "canvas_layout_place"] },
8
+ } as const;
9
+
10
+ export default defineActor(contract, {
11
+ functions: {},
12
+ onInput: () => {
13
+ throw new Error("The roster answers only starts; start it again from the run menu or with run_script_start.");
14
+ },
15
+ onStart: async (_start, context) => {
16
+ const actors = await context.functions.actor_list({});
17
+ const others = actors.filter((actor) => actor.kind !== "human" && actor.handle !== context.actor.handle);
18
+ const handles = others.map((actor) => `@${actor.handle}`);
19
+ const summary = handles.length === 0 ? "No other participants yet." : `${handles.length} participant${handles.length === 1 ? "" : "s"}: ${handles.join(", ")}.`;
20
+ const notebook = await context.functions.actor_program_ensure({ name: "notebook" });
21
+ await context.functions.actor_input({ actor: `@${notebook.handle}`, content: `Roster: ${summary}` });
22
+ await context.functions.canvas_layout_place({ entity: "app:notebook/main" });
23
+ context.state.replace({ reports: (context.state.read().reports ?? 0) + 1 });
24
+ context.finish({ actors: others.map(({ handle, kind, lifecycle }) => ({ handle, kind, lifecycle })) }, { summary });
25
+ },
26
+ });
@@ -0,0 +1,47 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createTestContext } from "@ragents/server/testing";
4
+ import program from "../src/server.ts";
5
+
6
+ const start = { input: null, options: {}, embedded: true, startedBy: "owner-id", count: 1 };
7
+ const actor = (handle: string, kind: "human" | "agent" | "script", lifecycle = "idle") =>
8
+ ({ id: `id-${handle}`, handle, displayName: handle, kind, lifecycle, createdBy: null, description: null, toolCount: null });
9
+
10
+ const roster = (actors: ReturnType<typeof actor>[], state: { reports?: number } = {}) => {
11
+ const calls: { name: string; input: unknown }[] = [];
12
+ const record = <T>(name: string, answer: T) => (input: unknown): T => { calls.push({ name, input }); return answer; };
13
+ const context = createTestContext<{ reports?: number }>({ state, functions: {
14
+ actor_list: record("actor_list", actors),
15
+ actor_program_ensure: record("actor_program_ensure", { actorId: "id-notebook", handle: "notebook", status: "active" as const }),
16
+ actor_input: record("actor_input", []),
17
+ canvas_layout_place: record("canvas_layout_place", { placed: true }),
18
+ } });
19
+ return { calls, context };
20
+ };
21
+
22
+ test("reports the other participants as result and summary and notes them in the shared notebook", async () => {
23
+ const { calls, context } = roster([actor("owner", "human"), actor("coordinator", "agent", "running"), actor("helper", "agent"), actor("test", "script")]);
24
+ await program.onStart!(start, context);
25
+ assert.deepEqual(context.finished, [{
26
+ result: { actors: [{ handle: "coordinator", kind: "agent", lifecycle: "running" }, { handle: "helper", kind: "agent", lifecycle: "idle" }] },
27
+ summary: "2 participants: @coordinator, @helper.",
28
+ }]);
29
+ assert.deepEqual(calls.slice(1).map((call) => [call.name, call.input]), [
30
+ ["actor_program_ensure", { name: "notebook" }],
31
+ ["actor_input", { actor: "@notebook", content: "Roster: 2 participants: @coordinator, @helper." }],
32
+ ["canvas_layout_place", { entity: "app:notebook/main" }],
33
+ ]);
34
+ assert.deepEqual(context.state.read(), { reports: 1 });
35
+ });
36
+
37
+ test("a run without other participants still gets a result, and every start counts", async () => {
38
+ const { context } = roster([actor("owner", "human"), actor("test", "script")], { reports: 2 });
39
+ await program.onStart!({ ...start, embedded: false, count: 3 }, context);
40
+ assert.deepEqual(context.finished, [{ result: { actors: [] }, summary: "No other participants yet." }]);
41
+ assert.deepEqual(context.state.read(), { reports: 3 });
42
+ });
43
+
44
+ test("an ordinary message is refused", async () => {
45
+ const { context } = roster([]);
46
+ await assert.rejects(async () => program.onInput!({ id: "input-1", content: "hello", artifactIds: [], sourceEventIds: [], subscriptionId: null, event: null }, context), /answers only starts/);
47
+ });
@@ -47,6 +47,11 @@ export const usage = (): string => `Usage: ragents <command> [arguments]
47
47
  stop <run> --run [--profile <p>]
48
48
  Emergency stop: cancel all turns and stop all actors of the run
49
49
  stop --host [--profile <p>] stop the remembered host
50
+ script <run> [--profile <p>] [--json]
51
+ List the run scripts of the profile and whether each can start in the running run.
52
+ script <run> <entry> [--input <json>] [--profile <p>] [--json]
53
+ Start the run script inside the run, as its owner; prints its actor and which start of it
54
+ this is. Only a script whose RUN.md sets embeddable: true joins a running run.
50
55
  plugin build <folder...> [--out <o>] [--watch] [--no-typecheck]
51
56
  Build plugin source folders into bundles; details with plugin --help.
52
57
 
@@ -67,12 +72,14 @@ export type AgentCommand =
67
72
  | { readonly kind: "journal"; readonly profile: string; readonly runId: string; readonly json: boolean; readonly tools: boolean }
68
73
  | { readonly kind: "stop"; readonly profile: string; readonly runId: string }
69
74
  | { readonly kind: "stop-run"; readonly profile: string; readonly runId: string }
70
- | { readonly kind: "stop-host"; readonly profile: string };
75
+ | { readonly kind: "stop-host"; readonly profile: string }
76
+ | { readonly kind: "script"; readonly profile: string; readonly runId: string; readonly entry: string | undefined; readonly input: unknown; readonly json: boolean };
71
77
 
72
78
  interface Flags {
73
79
  readonly profile: string;
74
80
  readonly entry: string | undefined;
75
81
  readonly workstation: string | undefined;
82
+ readonly input: string | undefined;
76
83
  readonly json: boolean;
77
84
  readonly tools: boolean;
78
85
  readonly host: boolean;
@@ -80,7 +87,7 @@ interface Flags {
80
87
  readonly positional: readonly string[];
81
88
  }
82
89
 
83
- const VALUE_FLAGS = new Set(["--profile", "--entry", "--workstation"]);
90
+ const VALUE_FLAGS = new Set(["--profile", "--entry", "--workstation", "--input"]);
84
91
 
85
92
  const scan = (argv: readonly string[], allowed: readonly string[]): Flags => {
86
93
  const positional: string[] = [];
@@ -106,6 +113,7 @@ const scan = (argv: readonly string[], allowed: readonly string[]): Flags => {
106
113
  profile: values.get("--profile") ?? defaultProfile(),
107
114
  entry: values.get("--entry"),
108
115
  workstation: values.get("--workstation"),
116
+ input: values.get("--input"),
109
117
  json: switches.has("--json"),
110
118
  tools: switches.has("--tools"),
111
119
  host: switches.has("--host"),
@@ -158,9 +166,26 @@ export const parseArguments = (argv: readonly string[]): AgentCommand => {
158
166
  if (!RUN_ID_PATTERN.test(runId)) throw new Error(`Invalid run id: ${runId}`);
159
167
  return { kind: flags.run ? "stop-run" : "stop", profile: flags.profile, runId };
160
168
  }
169
+ if (command === "script") {
170
+ const flags = scan(rest, ["--profile", "--input", "--json"]);
171
+ const [runId, entry, ...extra] = flags.positional;
172
+ if (!runId) throw new Error(`script needs <run>, optionally followed by <entry>.\n\n${usage()}`);
173
+ if (extra.length > 0) throw new Error(`script takes at most two values, not ${flags.positional.length}.`);
174
+ if (!RUN_ID_PATTERN.test(runId)) throw new Error(`Invalid run id: ${runId}`);
175
+ if (flags.input !== undefined && entry === undefined) throw new Error("--input needs <entry>, the run script it starts.");
176
+ return { kind: "script", profile: flags.profile, runId, entry, input: flags.input === undefined ? null : parsedInput(flags.input), json: flags.json };
177
+ }
161
178
  throw new Error(command ? `Unknown command: ${command}\n\n${usage()}` : usage());
162
179
  };
163
180
 
181
+ const parsedInput = (text: string): unknown => {
182
+ try {
183
+ return JSON.parse(text);
184
+ } catch (error) {
185
+ throw new Error(`--input is not JSON: ${error instanceof Error ? error.message : String(error)}`);
186
+ }
187
+ };
188
+
164
189
  export const loadProfile = (selection: string, root = hostRoot()): Promise<ProfileTarget> => selectProfileTarget(selection, root);
165
190
 
166
191
  const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
@@ -544,6 +569,24 @@ const stopCommand = async (command: Extract<AgentCommand, { kind: "stop" | "stop
544
569
  return 0;
545
570
  };
546
571
 
572
+ const scriptCommand = async (command: Extract<AgentCommand, { kind: "script" }>, write: LineWriter): Promise<number> => {
573
+ const target = await loadProfile(command.profile);
574
+ const baseUrl = await addressOf(target);
575
+ if (!await healthy(baseUrl)) throw new Error(`No RAgents server responds at ${baseUrl}; start it with ragents run.`);
576
+ const rpc = client(baseUrl);
577
+ if (command.entry === undefined) {
578
+ const scripts = await withLoginHint(() => rpc.call(coreContracts.runs.scripts, { runId: command.runId }));
579
+ for (const script of scripts) {
580
+ write(command.json ? JSON.stringify(script) : `${script.id} ${script.title}${script.available ? "" : ` (not available: ${script.reason ?? "no reason given"})`}`);
581
+ }
582
+ return 0;
583
+ }
584
+ const started = await withLoginHint(() => rpc.call(coreContracts.runs.startScript, { runId: command.runId, entry: command.entry!, input: command.input }));
585
+ write(command.json ? JSON.stringify(started) : `script: @${started.handle}, start ${started.count}`);
586
+ write(`run: ${command.runId}`);
587
+ return 0;
588
+ };
589
+
547
590
  const alive = (pid: number): boolean => {
548
591
  try {
549
592
  process.kill(pid, 0);
@@ -596,6 +639,7 @@ export const execute = async (command: AgentCommand, write: LineWriter = toStdou
596
639
  if (command.kind === "send") return sendCommand(command, write);
597
640
  if (command.kind === "journal") return journalCommand(command, write);
598
641
  if (command.kind === "stop" || command.kind === "stop-run") return stopCommand(command);
642
+ if (command.kind === "script") return scriptCommand(command, write);
599
643
  return stopHostCommand(command);
600
644
  };
601
645
 
@@ -17,6 +17,7 @@ export const commands = {
17
17
  send: AGENT_CLI,
18
18
  journal: AGENT_CLI,
19
19
  stop: AGENT_CLI,
20
+ script: AGENT_CLI,
20
21
  };
21
22
 
22
23
  const usage = `Usage: ragents <command> [arguments]
@@ -25,6 +26,7 @@ const usage = `Usage: ragents <command> [arguments]
25
26
  send <run> "<text>" follow-up task in the same run
26
27
  journal <run> [--tools] read the history of a run
27
28
  stop <run> | stop --host cancel the running turn or stop the remembered host
29
+ script <run> [<entry>] list the run scripts or start one inside the run (--input <json>)
28
30
  connect <server-url> fetch the client profile with its bundles and start the local server with it
29
31
  start <profile|path> start a profile of the package, an own profile file or a
30
32
  fetched revision; the UI comes prebuilt
@@ -33,7 +35,7 @@ const usage = `Usage: ragents <command> [arguments]
33
35
  plugin build <folder...> build plugin source folders into bundles (--out, --watch)
34
36
  --help, help this usage
35
37
 
36
- run, send, journal and stop take another profile with --profile <profile|path>: a name
38
+ run, send, journal, stop and script take another profile with --profile <profile|path>: a name
37
39
  next to the host or the path to an own ragents.config.<profile>.ts. RAGENTS_PROFILE sets
38
40
  the same for all commands of a shell; if the profile requires sign-in, the user's
39
41
  personal token goes into RAGENTS_TOKEN.