pi-roundtable 0.9.11 → 0.9.12
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 +7 -0
- package/docs/plugins.md +5 -2
- package/package.json +1 -1
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-team.ts +1 -0
- package/src/core/contract/providers.ts +5 -1
- package/src/core/runtime/runtime-plugin.ts +3 -0
- package/src/core/runtime/runtime-types.ts +8 -0
- package/src/core/runtime/session-factory.ts +21 -0
- package/src/core/shared/generate-image-tool.ts +173 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.12] - 2026-10-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `generate_image({ prompt, reference_paths? })`: a core session tool, offered to agent and owner sessions only when a plugin fills the `images` provider, that draws a new picture from a description, saves it under `<scratch dir or workspace>/drawings/`, attaches it to the reply, and names the saved path. Up to 4 reference images (PNG, JPEG, WebP or GIF by content, 20 MiB each, 40 MiB together) resolve inside the workspace or the scratch dir like `attach_file` paths. A provider error is a tool error with its message. Owner-only by default; the agents' prompt names it only when offered.
|
|
13
|
+
- `ImageDrawer` takes an optional third argument `{ signal? }`, the asking turn's abort signal; an existing two-argument drawer still fits.
|
|
14
|
+
|
|
8
15
|
## [0.9.11] - 2026-10-11
|
|
9
16
|
|
|
10
17
|
### Added
|
package/docs/plugins.md
CHANGED
|
@@ -693,6 +693,8 @@ A Pi package should use the host's peer dependency on `pi-roundtable`, not bundl
|
|
|
693
693
|
Transient `SessionContext.runTask` tasks return only text and cannot attach to their parent's reply.
|
|
694
694
|
|
|
695
695
|
The model attaches a file it already has on disk with the core's `attach_file({ path, filename? })`. A session with a workspace (`SessionContext.workspace`, an agent's) registers it; the path must resolve, symlinks followed, inside the workspace or the scratch dir, a relative path resolves in the workspace, and `filename` defaults to the path's base name. It queues the file with `attachReplyFile`, so `REPLY_FILE_LIMITS` apply and a surface without reply files refuses it as a tool error; its result tells the model the file will appear with its reply. It holds nothing, since it only posts in the channel the turn answers in, and its tier defaults to the owner, like any tool the core's table does not name.
|
|
696
|
+
|
|
697
|
+
With an `images` provider, agent and owner sessions also offer the core's `generate_image({ prompt, reference_paths? })`, which draws a new picture from a description (`prompt`, at most 4,000 characters). `reference_paths` lists up to 4 local images, checked like `attach_file` paths (inside the workspace or the scratch dir, symlinks followed, regular files only), each a PNG, JPEG, WebP or GIF by content, at most 20 MiB each and 40 MiB together; a refused reference is a tool error before the provider is called. It passes them to the provider as `ReferenceImage`s with the turn's signal, saves the picture under `<scratch dir or workspace>/drawings/generated-<ISO time>.<ext>`, and attaches it with `attachReplyFile`; when it cannot be attached the file is kept and the result says so. The result names the saved path, never the bytes, so the model can send it on or edit it. A provider error, such as a refusal, is a tool error carrying the provider's message. It holds nothing, and its tier defaults to the owner; `toolTiers` lowers it. The agents' prompt mentions it only when it is offered.
|
|
696
698
|
The Discord tools that upload, `discord_send_message` and `discord_edit_message`, take each file as exactly one of `path` (a local file, read when the call runs, with the same roots; `filename` defaults to its base name) or `dataBase64` with a `filename`, at most 8 MiB in all. A session without a workspace takes only `dataBase64`, and so does remote MCP, whose schemas stay inline only. A path outside the roots, a missing file, or a directory is a tool error naming the path. A held call's card (`approvalCard(call, workspace?, limits?)`) lists each file it sends by path with the file's size now, never its bytes, and shows an input within `limits.totalChars` (1,500) whole; a longer input has its string values cut, longest first and never below `limits.valueChars` (200), to their start plus `… [N chars]` (N in code points) until it fits, and if it still does not, the rest is dropped and the keys no longer shown are listed. `approvalDetails(call, workspace?)` gives the same call as data (`{ action, tool, input, files? }`, the whole input, files as `{ path, bytes? }`); the gate passes it to `Prompts.confirm` as `details`, for a surface that is not text.
|
|
697
699
|
Calling the helper outside `context.turns.run` or an agent-team turn, or after that turn finishes, throws `ReplyFileError`; direct standalone runtime calls have no reply collector.
|
|
698
700
|
Await work that produces files before returning from the tool.
|
|
@@ -1279,10 +1281,11 @@ An unknown or misspelled slot stops startup with an error listing the valid name
|
|
|
1279
1281
|
| Slot | The core's default | Your replacement |
|
|
1280
1282
|
|---|---|---|
|
|
1281
1283
|
| `judge` | Asks the configured model small questions: does this reply approve the held actions, how hard is this turn, which agents does this group message concern | An object with `askYesNo`, `askChoice`, and `askScore` |
|
|
1282
|
-
| `images` | No drawing; each agent gets an avatar generated from its display name | `async (prompt, references) => bytes` returning PNG bytes |
|
|
1284
|
+
| `images` | No drawing; each agent gets an avatar generated from its display name, and no session offers `generate_image` | `async (prompt, references, options?) => bytes` returning PNG, JPEG, WebP or GIF bytes; `options.signal` aborts with the turn that asked |
|
|
1283
1285
|
| `runtime` | None: the runtime plugin builds the Pi runtime itself | `(deps) => runtime`, an [`AgentRuntime`](#the-runtime-slot-replace-pi) that runs every conversation |
|
|
1284
1286
|
|
|
1285
|
-
|
|
1287
|
+
A filled `images` slot draws agents' avatars and also powers the core's [`generate_image`](#attach-files-to-the-agents-reply) tool.
|
|
1288
|
+
When no `images` provider is configured, `agent_create` has no `avatar_prompt` parameter, `agent_avatar` is unavailable, and no session offers `generate_image`.
|
|
1286
1289
|
The owner's profile panel reports the missing provider and offers no redraw option.
|
|
1287
1290
|
Each agent gets a picture generated from its display name and the assistant's icon, so you can tell agents apart.
|
|
1288
1291
|
`bunx roundtable doctor` reports whether the slot is filled.
|
package/package.json
CHANGED
|
@@ -25,6 +25,8 @@ export function agentSystemPrompt(input: {
|
|
|
25
25
|
speaker?: Speaker;
|
|
26
26
|
/** Whether agents can redraw avatars; false without an image provider. Default true. */
|
|
27
27
|
avatars?: boolean;
|
|
28
|
+
/** Whether the session offers generate_image; true only with an image provider. Default false. */
|
|
29
|
+
images?: boolean;
|
|
28
30
|
}): string {
|
|
29
31
|
const { agent, coordinator, shared, workDir, owner, shellUser, group } =
|
|
30
32
|
input;
|
|
@@ -37,7 +39,7 @@ export function agentSystemPrompt(input: {
|
|
|
37
39
|
`You are "${agent.displayName}" (agent name \`${agent.name}\`), one of ${o.name}'s agents in ${o.his} Discord agent server. Each agent owns one channel and one conversation; you all share ${o.his} tools and ${o.his} memory.`,
|
|
38
40
|
roleText(agent, coordinator, who),
|
|
39
41
|
`The team: agent_list shows every agent and group; agent_get and agent_update read and improve any agent's prompt, display name, model, and thinking level, yours included; agent_create adds an agent with its own channel; ${input.avatars === false ? "" : "agent_avatar redraws an avatar; "}schedule_list with agent reads another agent's schedules. A message marked as coming from another agent is that agent speaking, not ${w.name}; only ${w.name} approves held actions.`,
|
|
40
|
-
`Your shell and file tools run on ${o.his} VPS as the user ${shellUser}, in the shared workspace ${workDir}.${input.scratchDir ? ` Write temporary files under the scratch dir ${input.scratchDir} ($TMPDIR), not elsewhere in /tmp.` : ""} Commands that are destructive or change the system, and writes outside the workspace${input.scratchDir ? " and the scratch dir" : ""}, are held for ${w.his} confirmation: tell ${w.him} exactly what will run and ask ${w.him} to confirm. To show a file you have, attach it to your reply with attach_file or give its path to discord_send_message; never read an image or base64 a file to send it
|
|
42
|
+
`Your shell and file tools run on ${o.his} VPS as the user ${shellUser}, in the shared workspace ${workDir}.${input.scratchDir ? ` Write temporary files under the scratch dir ${input.scratchDir} ($TMPDIR), not elsewhere in /tmp.` : ""} Commands that are destructive or change the system, and writes outside the workspace${input.scratchDir ? " and the scratch dir" : ""}, are held for ${w.his} confirmation: tell ${w.him} exactly what will run and ask ${w.him} to confirm. To show a file you have, attach it to your reply with attach_file or give its path to discord_send_message; never read an image or base64 a file to send it.${input.images ? " To make a new picture, describe it to generate_image, which saves it and attaches it to your reply." : ""}`,
|
|
41
43
|
];
|
|
42
44
|
if (group) {
|
|
43
45
|
const others = group.members
|
|
@@ -56,10 +56,14 @@ export interface ReferenceImage {
|
|
|
56
56
|
mimeType: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Draws one image from a prompt and reference images; returns PNG or other decodable bytes.
|
|
61
|
+
* `options.signal`, when given, aborts with the turn that asked; a drawer may ignore it.
|
|
62
|
+
*/
|
|
60
63
|
export type ImageDrawer = (
|
|
61
64
|
prompt: string,
|
|
62
65
|
references: readonly ReferenceImage[],
|
|
66
|
+
options?: { signal?: AbortSignal },
|
|
63
67
|
) => Promise<Uint8Array>;
|
|
64
68
|
|
|
65
69
|
/** The replaceable parts the core runs on; a plugin provides at most the ones it replaces. */
|
|
@@ -168,6 +168,9 @@ export function runtimePlugin(
|
|
|
168
168
|
}),
|
|
169
169
|
confirmations: heldActions,
|
|
170
170
|
toolTiers: context.toolTiers,
|
|
171
|
+
...(providers.filled.has("images")
|
|
172
|
+
? { images: providers.images }
|
|
173
|
+
: {}),
|
|
171
174
|
stopping: () => context.queue.closed,
|
|
172
175
|
get agents() {
|
|
173
176
|
return agents();
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ExtensionFactory,
|
|
5
5
|
ModelRuntime,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ImageDrawer } from "../contract/providers.ts";
|
|
7
8
|
import type { AgentSessions, LoadedSkill } from "../contract/runtime.ts";
|
|
8
9
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
9
10
|
import type { InterimTextMode } from "../domain/interim.ts";
|
|
@@ -97,6 +98,8 @@ export interface PiAgentRuntimeOptions {
|
|
|
97
98
|
interimPrimaryChars?: number;
|
|
98
99
|
/** How long a new session waits for its MCP tools to register. */
|
|
99
100
|
mcpConnectTimeoutMs?: number;
|
|
101
|
+
/** The host's image provider, when one fills the slot; sessions then offer `generate_image`. */
|
|
102
|
+
images?: ImageDrawer;
|
|
100
103
|
/** Defaults to true: bridge shared turns refuse raw private exchanges inaccessible to the reader. */
|
|
101
104
|
memory?: boolean;
|
|
102
105
|
}
|
|
@@ -113,6 +116,8 @@ export interface CoreExtensions {
|
|
|
113
116
|
readAttachment: ExtensionFactory;
|
|
114
117
|
/** `attach_file`, for a session with a workspace. */
|
|
115
118
|
attachFile?: ExtensionFactory;
|
|
119
|
+
/** `generate_image`, for a host with an image provider. */
|
|
120
|
+
generateImage?: ExtensionFactory;
|
|
116
121
|
confirmationGate: ExtensionFactory;
|
|
117
122
|
askUser: ExtensionFactory;
|
|
118
123
|
selfCompactGuard: ExtensionFactory;
|
|
@@ -144,6 +149,9 @@ export function sessionExtensions(
|
|
|
144
149
|
...(core.attachFile
|
|
145
150
|
? [{ name: "attach-file", factory: core.attachFile }]
|
|
146
151
|
: []),
|
|
152
|
+
...(core.generateImage
|
|
153
|
+
? [{ name: "generate-image", factory: core.generateImage }]
|
|
154
|
+
: []),
|
|
147
155
|
{ name: "confirmation-gate", factory: core.confirmationGate },
|
|
148
156
|
{ name: "ask-user", factory: core.askUser },
|
|
149
157
|
{ name: "self-compact-guard", factory: core.selfCompactGuard },
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ToolDefinition,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { channelSegment } from "../attachments/attachment-dir.ts";
|
|
13
|
+
import type { ImageDrawer } from "../contract/providers.ts";
|
|
13
14
|
import type { ClientPort } from "../domain/client.ts";
|
|
14
15
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
15
16
|
import { ConfigError } from "../domain/errors.ts";
|
|
@@ -28,8 +29,13 @@ import {
|
|
|
28
29
|
ATTACH_FILE_TOOL,
|
|
29
30
|
attachFileExtension,
|
|
30
31
|
} from "../shared/attach-file-tool.ts";
|
|
32
|
+
import {
|
|
33
|
+
GENERATE_IMAGE_TOOL,
|
|
34
|
+
generateImageExtension,
|
|
35
|
+
} from "../shared/generate-image-tool.ts";
|
|
31
36
|
import { packageDir } from "../shared/package-dir.ts";
|
|
32
37
|
import { readAttachmentExtension } from "../shared/read-attachment-tool.ts";
|
|
38
|
+
import type { WorkspaceRoots } from "../shared/workspace-files.ts";
|
|
33
39
|
import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
34
40
|
import { currentToolNames } from "../tool-tiers.ts";
|
|
35
41
|
import { toolTurn } from "../tool-turn.ts";
|
|
@@ -56,6 +62,7 @@ import type { PromptSlot } from "./prompt-slot.ts";
|
|
|
56
62
|
import type {
|
|
57
63
|
ChannelSession,
|
|
58
64
|
CompactionEnd,
|
|
65
|
+
CoreExtensions,
|
|
59
66
|
PiAgentRuntimeOptions,
|
|
60
67
|
} from "./runtime-types.ts";
|
|
61
68
|
import {
|
|
@@ -142,6 +149,8 @@ export class SessionFactory {
|
|
|
142
149
|
ASK_USER_TOOL,
|
|
143
150
|
// Registered only by a session with a workspace.
|
|
144
151
|
ATTACH_FILE_TOOL,
|
|
152
|
+
// Registered only with an image provider.
|
|
153
|
+
GENERATE_IMAGE_TOOL,
|
|
145
154
|
...groups
|
|
146
155
|
.filter((group) => selection.groups.includes(group.name))
|
|
147
156
|
.flatMap((group) => group.tools),
|
|
@@ -381,6 +390,10 @@ export class SessionFactory {
|
|
|
381
390
|
...(context.workspace
|
|
382
391
|
? { attachFile: attachFileExtension(context.workspace) }
|
|
383
392
|
: {}),
|
|
393
|
+
...imageTools(
|
|
394
|
+
this.#options.images,
|
|
395
|
+
context.workspace ?? { workspace: cwd },
|
|
396
|
+
),
|
|
384
397
|
confirmationGate: confirmationGateExtension(gate, slot, (signal) =>
|
|
385
398
|
toolTurn(context, signal),
|
|
386
399
|
),
|
|
@@ -442,6 +455,14 @@ export class SessionFactory {
|
|
|
442
455
|
}
|
|
443
456
|
}
|
|
444
457
|
|
|
458
|
+
/** `generate_image` for a session of a host with an image provider; nothing without one. */
|
|
459
|
+
export function imageTools(
|
|
460
|
+
images: ImageDrawer | undefined,
|
|
461
|
+
roots: WorkspaceRoots,
|
|
462
|
+
): Pick<CoreExtensions, "generateImage"> {
|
|
463
|
+
return images ? { generateImage: generateImageExtension(images, roots) } : {};
|
|
464
|
+
}
|
|
465
|
+
|
|
445
466
|
/** Pi's bash tool with TMPDIR set to `scratchDir`, so `mktemp` and tools write there. */
|
|
446
467
|
export function scratchBash(cwd: string, scratchDir: string): ToolDefinition {
|
|
447
468
|
// SAFETY: Pi's bash definition is a ToolDefinition typed by its own parameter schema, which the session takes as any custom tool.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import type { ImageDrawer, ReferenceImage } from "../contract/providers.ts";
|
|
6
|
+
import {
|
|
7
|
+
attachReplyFile,
|
|
8
|
+
REPLY_FILE_LIMITS,
|
|
9
|
+
ReplyFileError,
|
|
10
|
+
} from "../reply-files.ts";
|
|
11
|
+
import { toolError, toolText } from "./tool-result.ts";
|
|
12
|
+
import {
|
|
13
|
+
findWorkspaceFile,
|
|
14
|
+
readWorkspaceFile,
|
|
15
|
+
WorkspaceFileError,
|
|
16
|
+
type WorkspaceRoots,
|
|
17
|
+
} from "./workspace-files.ts";
|
|
18
|
+
|
|
19
|
+
export const GENERATE_IMAGE_TOOL = "generate_image";
|
|
20
|
+
|
|
21
|
+
/** The limits on the reference images one call passes to the drawer. */
|
|
22
|
+
export const REFERENCE_IMAGE_LIMITS = Object.freeze({
|
|
23
|
+
maxImages: 4,
|
|
24
|
+
maxImageBytes: 20 * 1024 * 1024,
|
|
25
|
+
maxTotalBytes: 40 * 1024 * 1024,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const MAX_PROMPT_CHARS = 4000;
|
|
29
|
+
|
|
30
|
+
/** The image type of `data` by its content, or undefined for anything else. */
|
|
31
|
+
export function imageTypeOf(
|
|
32
|
+
data: Uint8Array,
|
|
33
|
+
): { mimeType: string; ext: string } | undefined {
|
|
34
|
+
const starts = (bytes: readonly number[], at = 0) =>
|
|
35
|
+
bytes.every((byte, i) => data[at + i] === byte);
|
|
36
|
+
if (starts([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
37
|
+
return { mimeType: "image/png", ext: "png" };
|
|
38
|
+
if (starts([0xff, 0xd8, 0xff])) return { mimeType: "image/jpeg", ext: "jpg" };
|
|
39
|
+
if (starts([0x47, 0x49, 0x46, 0x38]))
|
|
40
|
+
return { mimeType: "image/gif", ext: "gif" };
|
|
41
|
+
if (starts([0x52, 0x49, 0x46, 0x46]) && starts([0x57, 0x45, 0x42, 0x50], 8))
|
|
42
|
+
return { mimeType: "image/webp", ext: "webp" };
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readReferences(
|
|
47
|
+
paths: readonly string[],
|
|
48
|
+
roots: WorkspaceRoots,
|
|
49
|
+
): Promise<ReferenceImage[]> {
|
|
50
|
+
const { maxImages, maxImageBytes, maxTotalBytes } = REFERENCE_IMAGE_LIMITS;
|
|
51
|
+
if (paths.length > maxImages)
|
|
52
|
+
throw new WorkspaceFileError(
|
|
53
|
+
`${paths.length} reference images given; at most ${maxImages} are allowed.`,
|
|
54
|
+
);
|
|
55
|
+
const files = await Promise.all(
|
|
56
|
+
paths.map((path) => findWorkspaceFile(path, roots)),
|
|
57
|
+
);
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
if (file.size > maxImageBytes)
|
|
61
|
+
throw new WorkspaceFileError(
|
|
62
|
+
`${file.path} is ${file.size} bytes; a reference image may contain at most ${maxImageBytes} bytes.`,
|
|
63
|
+
);
|
|
64
|
+
total += file.size;
|
|
65
|
+
}
|
|
66
|
+
if (total > maxTotalBytes)
|
|
67
|
+
throw new WorkspaceFileError(
|
|
68
|
+
`the reference images total ${total} bytes; at most ${maxTotalBytes} are allowed.`,
|
|
69
|
+
);
|
|
70
|
+
const references: ReferenceImage[] = [];
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const data = await readWorkspaceFile(file, maxImageBytes);
|
|
73
|
+
const type = imageTypeOf(data);
|
|
74
|
+
if (!type)
|
|
75
|
+
throw new WorkspaceFileError(
|
|
76
|
+
`${file.path} is not a PNG, JPEG, WebP or GIF image.`,
|
|
77
|
+
);
|
|
78
|
+
references.push({
|
|
79
|
+
data: Buffer.from(data).toString("base64"),
|
|
80
|
+
mimeType: type.mimeType,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return references;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Registers `generate_image`, which draws a new picture with the host's image provider, saves it
|
|
88
|
+
* under `<scratch dir or workspace>/drawings`, and attaches it to the turn's reply.
|
|
89
|
+
*/
|
|
90
|
+
export function generateImageExtension(
|
|
91
|
+
draw: ImageDrawer,
|
|
92
|
+
roots: WorkspaceRoots,
|
|
93
|
+
): ExtensionFactory {
|
|
94
|
+
return (pi) => {
|
|
95
|
+
pi.registerTool({
|
|
96
|
+
name: GENERATE_IMAGE_TOOL,
|
|
97
|
+
label: "Generate image",
|
|
98
|
+
description:
|
|
99
|
+
'Generate a new picture from a description. Describe the subject, style and composition; say "no text" when you will add text to it later. The picture is saved to a file whose path the result names, and attached to your reply.',
|
|
100
|
+
parameters: Type.Object(
|
|
101
|
+
{
|
|
102
|
+
prompt: Type.String({
|
|
103
|
+
minLength: 1,
|
|
104
|
+
maxLength: MAX_PROMPT_CHARS,
|
|
105
|
+
description:
|
|
106
|
+
"What to draw: subject, style, composition, and anything to leave out.",
|
|
107
|
+
}),
|
|
108
|
+
reference_paths: Type.Optional(
|
|
109
|
+
Type.Array(Type.String({ minLength: 1, maxLength: 4096 }), {
|
|
110
|
+
maxItems: REFERENCE_IMAGE_LIMITS.maxImages,
|
|
111
|
+
description:
|
|
112
|
+
"Paths of PNG, JPEG, WebP or GIF images inside the workspace or the scratch dir to draw from.",
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
{ additionalProperties: false },
|
|
117
|
+
),
|
|
118
|
+
execute: async (_toolCallId, params, signal) => {
|
|
119
|
+
let references: ReferenceImage[];
|
|
120
|
+
try {
|
|
121
|
+
references = await readReferences(
|
|
122
|
+
params.reference_paths ?? [],
|
|
123
|
+
roots,
|
|
124
|
+
);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof WorkspaceFileError)
|
|
127
|
+
return toolError(`Not drawn: ${error.message}`);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
signal?.throwIfAborted();
|
|
131
|
+
let data: Uint8Array;
|
|
132
|
+
try {
|
|
133
|
+
data = await draw(
|
|
134
|
+
params.prompt,
|
|
135
|
+
references,
|
|
136
|
+
signal ? { signal } : {},
|
|
137
|
+
);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
signal?.throwIfAborted();
|
|
140
|
+
return toolError(
|
|
141
|
+
`The image provider did not draw it: ${error instanceof Error ? error.message : String(error)}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
signal?.throwIfAborted();
|
|
145
|
+
const type = imageTypeOf(data);
|
|
146
|
+
if (!type)
|
|
147
|
+
return toolError(
|
|
148
|
+
"The image provider returned something that is not a PNG, JPEG, WebP or GIF image.",
|
|
149
|
+
);
|
|
150
|
+
const dir = join(roots.scratchDir ?? roots.workspace, "drawings");
|
|
151
|
+
await mkdir(dir, { recursive: true });
|
|
152
|
+
const name = `generated-${new Date().toISOString().replace(/[:.]/g, "-")}.${type.ext}`;
|
|
153
|
+
const path = join(dir, name);
|
|
154
|
+
await writeFile(path, data);
|
|
155
|
+
try {
|
|
156
|
+
if (data.byteLength > REPLY_FILE_LIMITS.maxFileBytes)
|
|
157
|
+
throw new ReplyFileError(
|
|
158
|
+
`it is ${data.byteLength} bytes; a reply file may contain at most ${REPLY_FILE_LIMITS.maxFileBytes} bytes.`,
|
|
159
|
+
);
|
|
160
|
+
attachReplyFile({ name, data });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!(error instanceof ReplyFileError)) throw error;
|
|
163
|
+
return toolText(
|
|
164
|
+
`Drew the picture and saved it to ${path} (${data.byteLength} bytes), but it is not attached: ${error.message}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return toolText(
|
|
168
|
+
`Drew the picture, saved it to ${path} (${data.byteLength} bytes); it will appear with your reply.`,
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|