pi-roundtable-sandbox 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.24.1] - 2026-10-29
6
+
7
+ ### Added
8
+
9
+ - `sandbox({ replyText })`: rewrites each successful model reply on the host before it is split and posted (`(text, { channel, space }) => string | Promise<string>`), for example to convert stray Simplified Chinese to Traditional. Failure texts never pass through it, the worker's history keeps the model's own words, and a throw or a non-string fails the turn like any other failure. Host-only: no worker protocol change, no image rebuild. An empty result posts nothing, as an empty model reply does.
10
+ - `SandboxClaimOptions.reportFailure` receives the error as its second argument, and the plugin logs it with the channel.
11
+
5
12
  ## [0.24.0] - 2026-10-29
6
13
 
7
14
  The core peer range is now `>=0.24.0 <0.25.0`. Every new option is optional; with none set, behavior, mounts, files on disk, the worker protocol and the public API are as in 0.23.x, and `PiSandboxRuntime` is unchanged.
package/README.md CHANGED
@@ -177,6 +177,14 @@ Without `budgets` and `skills`, an oversized `prompt` is still cut at 8,192 byte
177
177
  Any other cleartext URL still needs `allowHttp`, and credentials in the URL stay refused.
178
178
  This applies to `modelUrl` only, not to MCP servers.
179
179
 
180
+ ### Rewriting replies on the host
181
+
182
+ `replyText(text, { channel, space })` rewrites each successful model reply on the host before it is split and posted, and may return a promise.
183
+ Use it for what the prompt cannot guarantee, such as converting a model's stray Simplified Chinese to Traditional.
184
+ It never sees failure texts, and the history the worker keeps holds the model's own words.
185
+ A throw or a value that is not a string fails the turn like any other failure, and the error is logged with the channel; an empty string posts nothing, as an empty model reply does.
186
+ It needs no new worker image.
187
+
180
188
  ### Worker image protocol
181
189
 
182
190
  The worker image carries the label `pi-roundtable-sandbox.protocol` with the worker protocol version (2 in this release, exported as `SANDBOX_PROTOCOL`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-sandbox",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Sealed guest channels and an allow-listed credential broker for pi-roundtable",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "@biomejs/biome": "2.5.15",
53
53
  "@types/bun": "1.4.3",
54
54
  "discord.js": "14.27.0",
55
- "pi-roundtable": "0.24.0",
55
+ "pi-roundtable": "0.24.1",
56
56
  "typebox": "1.3.37",
57
57
  "typescript": "7.0.2"
58
58
  }
package/src/claim.ts CHANGED
@@ -8,13 +8,25 @@ import { splitReply } from "pi-roundtable/kit";
8
8
  import type { SandboxChannelStore } from "./channel-store.ts";
9
9
  import { type SandboxRuntime, SandboxTurnRefused } from "./runtime.ts";
10
10
 
11
+ /**
12
+ * Rewrites a successful model reply before it is posted, such as converting script or stripping
13
+ * markup. It runs on the host, after the sealed turn and before the reply is split into chunks;
14
+ * failure texts never pass through it. A throw, or a value that is not a string, fails the turn.
15
+ */
16
+ export type SandboxReplyText = (
17
+ text: string,
18
+ context: { channel: ChannelKey; space?: string },
19
+ ) => string | Promise<string>;
20
+
11
21
  export interface SandboxClaimOptions {
12
22
  /** `owns` adds channels in configured guilds; without it only `has` decides. */
13
23
  channels: Pick<SandboxChannelStore, "has"> &
14
24
  Partial<Pick<SandboxChannelStore, "owns">>;
15
25
  runtime: Pick<SandboxRuntime, "runTurn" | "stop" | "startFresh">;
16
26
  surfaces: Pick<SurfacePort, "sendReply" | "startTyping">;
17
- reportFailure?: (channel: ChannelKey) => void;
27
+ /** Told of a failed turn, with what failed (a sandbox error, or replyText's own throw). */
28
+ reportFailure?: (channel: ChannelKey, error: unknown) => void;
29
+ replyText?: SandboxReplyText;
18
30
  }
19
31
 
20
32
  export function isSandboxAddress(message: InboundMessage): boolean {
@@ -26,6 +38,21 @@ export function isSandboxAddress(message: InboundMessage): boolean {
26
38
  );
27
39
  }
28
40
 
41
+ async function rewriteReply(
42
+ replyText: SandboxReplyText | undefined,
43
+ text: string,
44
+ message: InboundMessage,
45
+ ): Promise<string> {
46
+ if (!replyText) return text;
47
+ const rewritten = await replyText(text, {
48
+ channel: message.channel,
49
+ ...(message.space ? { space: message.space } : {}),
50
+ });
51
+ if (typeof rewritten !== "string")
52
+ throw new TypeError("replyText must return a string");
53
+ return rewritten;
54
+ }
55
+
29
56
  /** Outranks the agent server's priority 100; ignored messages never fall through to host agents. */
30
57
  export function sandboxClaim(options: SandboxClaimOptions): ChannelClaim {
31
58
  const owns = (channel: ChannelKey, space?: string): boolean =>
@@ -68,15 +95,16 @@ export function sandboxClaim(options: SandboxClaimOptions): ChannelClaim {
68
95
  text,
69
96
  { space: message.space },
70
97
  );
98
+ const posted = reply.ok
99
+ ? await rewriteReply(options.replyText, reply.text, message)
100
+ : "The sandbox turn failed.";
71
101
  await options.surfaces.sendReply(message.channel, {
72
- chunks: splitReply(
73
- reply.ok ? reply.text : "The sandbox turn failed.",
74
- ),
102
+ chunks: splitReply(posted),
75
103
  });
76
104
  } catch (error) {
77
105
  // A stop or a full queue is the runtime working as designed, not a failure.
78
106
  if (!(error instanceof SandboxTurnRefused))
79
- options.reportFailure?.(message.channel);
107
+ options.reportFailure?.(message.channel, error);
80
108
  await options.surfaces.sendReply(message.channel, {
81
109
  chunks: ["The sandbox turn failed or was stopped."],
82
110
  });
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export { SandboxChannelStore } from "./channel-store.ts";
27
27
  export {
28
28
  isSandboxAddress,
29
29
  type SandboxClaimOptions,
30
+ type SandboxReplyText,
30
31
  sandboxClaim,
31
32
  } from "./claim.ts";
32
33
  export { sandboxCommands } from "./commands.ts";
package/src/plugin.ts CHANGED
@@ -3,7 +3,7 @@ import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import { type ChannelKey, definePlugin, serviceKey } from "pi-roundtable";
4
4
  import { DISCORD } from "pi-roundtable/discord";
5
5
  import { SandboxChannelStore } from "./channel-store.ts";
6
- import { sandboxClaim } from "./claim.ts";
6
+ import { type SandboxReplyText, sandboxClaim } from "./claim.ts";
7
7
  import { sandboxCommands } from "./commands.ts";
8
8
  import { SandboxRuntime, type SandboxRuntimeOptions } from "./runtime.ts";
9
9
 
@@ -23,6 +23,12 @@ export interface SandboxOptions
23
23
  provider?: string;
24
24
  /** Read before each model call with its channel and speaker; never falls back to the host login. */
25
25
  apiKey?: SandboxRuntimeOptions["apiKey"];
26
+ /**
27
+ * Rewrites each successful model reply on the host before it is posted, for example to
28
+ * convert Simplified to Traditional Chinese. Failure texts are not passed to it; a throw
29
+ * fails the turn.
30
+ */
31
+ replyText?: SandboxReplyText;
26
32
  }
27
33
  export interface SandboxService {
28
34
  channels: SandboxChannelStore;
@@ -75,10 +81,11 @@ export function sandbox(options: SandboxOptions) {
75
81
  channels,
76
82
  runtime,
77
83
  surfaces: context.surfaces,
78
- reportFailure: (channel) =>
84
+ ...(options.replyText ? { replyText: options.replyText } : {}),
85
+ reportFailure: (channel, error) =>
79
86
  context.logger.error(
80
- { channel },
81
- "sandbox turn failed; inspect container cleanup and broker configuration",
87
+ { channel, err: error },
88
+ "sandbox turn failed; inspect the error, then container cleanup, broker configuration and replyText",
82
89
  ),
83
90
  }),
84
91
  ],