pi-roundtable-sandbox 0.7.2 → 0.7.3
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/README.md +125 -1
- package/package.json +15 -3
- package/src/broker.ts +151 -85
- package/src/directory-file.ts +48 -0
- package/src/index.ts +76 -0
- package/src/model-input.ts +1 -1
- package/src/pi-attachments.ts +159 -0
- package/src/pi-broker.ts +500 -0
- package/src/pi-container-driver.ts +194 -0
- package/src/pi-model-input.ts +219 -0
- package/src/pi-protocol.ts +141 -0
- package/src/pi-runtime.ts +396 -0
- package/src/protocol.ts +8 -0
- package/src/research-worker.ts +132 -0
- package/src/safe-fetch.ts +397 -0
- package/src/scoped-delegator.ts +120 -0
- package/worker/Dockerfile.deps.pi +4 -0
- package/worker/Dockerfile.pi +12 -0
- package/worker/pi-content.ts +16 -0
- package/worker/pi-main.ts +382 -0
- package/worker/pi-memory.ts +21 -0
- package/worker/pi-skills.ts +140 -0
- package/worker/pi-tools.ts +211 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
fstatSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { basename, extname, join } from "node:path";
|
|
11
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
type ReplyFile,
|
|
14
|
+
type SessionContext,
|
|
15
|
+
type ToolContribution,
|
|
16
|
+
withReplyFiles,
|
|
17
|
+
} from "pi-roundtable";
|
|
18
|
+
import { toolText } from "pi-roundtable/kit";
|
|
19
|
+
import type { TSchema } from "typebox";
|
|
20
|
+
import {
|
|
21
|
+
PI_MEDIA_LIMITS,
|
|
22
|
+
type PiToolResponse,
|
|
23
|
+
type PiTurnContext,
|
|
24
|
+
safeFileName,
|
|
25
|
+
validateImages,
|
|
26
|
+
} from "../src/pi-protocol.ts";
|
|
27
|
+
import { boundedText } from "../src/protocol.ts";
|
|
28
|
+
|
|
29
|
+
export interface PiWorkerToolSpec {
|
|
30
|
+
name: string;
|
|
31
|
+
label: string;
|
|
32
|
+
description: string;
|
|
33
|
+
parameters: TSchema;
|
|
34
|
+
}
|
|
35
|
+
export function saveToOutbox(
|
|
36
|
+
turn: PiTurnContext,
|
|
37
|
+
stem: string,
|
|
38
|
+
data: Uint8Array,
|
|
39
|
+
ext = "png",
|
|
40
|
+
): string {
|
|
41
|
+
if (
|
|
42
|
+
!safeFileName(stem) ||
|
|
43
|
+
!/^[a-z0-9]{1,10}$/.test(ext) ||
|
|
44
|
+
data.byteLength > PI_MEDIA_LIMITS.fileBytes ||
|
|
45
|
+
data.byteLength === 0
|
|
46
|
+
)
|
|
47
|
+
throw new Error("Invalid output file");
|
|
48
|
+
let name = `${stem}.${ext}`;
|
|
49
|
+
for (let n = 2; readdirSync(turn.outbox).includes(name); n++)
|
|
50
|
+
name = `${stem}-${n}.${ext}`;
|
|
51
|
+
writeFileSync(join(turn.outbox, name), data, { flag: "wx" });
|
|
52
|
+
return name;
|
|
53
|
+
}
|
|
54
|
+
export function boundedFile(path: string, limit: number): Uint8Array {
|
|
55
|
+
const fd = openSync(
|
|
56
|
+
path,
|
|
57
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
const info = fstatSync(fd);
|
|
61
|
+
if (!info.isFile() || info.size < 1 || info.size > limit)
|
|
62
|
+
throw new Error("Invalid or oversized file");
|
|
63
|
+
const data = Buffer.alloc(info.size + 1);
|
|
64
|
+
let count = 0;
|
|
65
|
+
while (count < data.length) {
|
|
66
|
+
const size = readSync(fd, data, count, data.length - count, null);
|
|
67
|
+
if (!size) break;
|
|
68
|
+
count += size;
|
|
69
|
+
}
|
|
70
|
+
if (count !== info.size) throw new Error("File changed while reading");
|
|
71
|
+
return data.subarray(0, count);
|
|
72
|
+
} finally {
|
|
73
|
+
closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const IMAGE_TYPES: Record<string, string> = {
|
|
77
|
+
".png": "image/png",
|
|
78
|
+
".jpg": "image/jpeg",
|
|
79
|
+
".jpeg": "image/jpeg",
|
|
80
|
+
".webp": "image/webp",
|
|
81
|
+
".gif": "image/gif",
|
|
82
|
+
};
|
|
83
|
+
export function brokerToolsExtension(
|
|
84
|
+
socket: string,
|
|
85
|
+
attachments: string,
|
|
86
|
+
turn: () => PiTurnContext,
|
|
87
|
+
specs: readonly PiWorkerToolSpec[],
|
|
88
|
+
): ExtensionFactory {
|
|
89
|
+
return (pi) => {
|
|
90
|
+
for (const spec of specs)
|
|
91
|
+
pi.registerTool({
|
|
92
|
+
...spec,
|
|
93
|
+
execute: async (_id, params, signal) => {
|
|
94
|
+
const input = { ...(params as Record<string, unknown>) };
|
|
95
|
+
if (
|
|
96
|
+
spec.name === "image_generate" &&
|
|
97
|
+
Array.isArray(input.reference_images)
|
|
98
|
+
) {
|
|
99
|
+
if (input.reference_images.length > 4)
|
|
100
|
+
throw new Error("At most four references");
|
|
101
|
+
input.reference_images = input.reference_images.map(
|
|
102
|
+
(name: unknown) => {
|
|
103
|
+
if (
|
|
104
|
+
typeof name !== "string" ||
|
|
105
|
+
!safeFileName(name) ||
|
|
106
|
+
basename(name) !== name
|
|
107
|
+
)
|
|
108
|
+
throw new Error("Invalid attachment name");
|
|
109
|
+
const mimeType = IMAGE_TYPES[extname(name).toLowerCase()];
|
|
110
|
+
if (!mimeType) throw new Error("Not an image attachment");
|
|
111
|
+
return {
|
|
112
|
+
data: Buffer.from(
|
|
113
|
+
boundedFile(
|
|
114
|
+
join(attachments, name),
|
|
115
|
+
PI_MEDIA_LIMITS.imageBytes,
|
|
116
|
+
),
|
|
117
|
+
).toString("base64"),
|
|
118
|
+
mimeType,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
validateImages(input.reference_images);
|
|
123
|
+
}
|
|
124
|
+
const response = await fetch(`http://broker/tools/${spec.name}`, {
|
|
125
|
+
unix: socket,
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: JSON.stringify({ input }),
|
|
129
|
+
...(signal ? { signal } : {}),
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok)
|
|
132
|
+
throw new Error(`Broker answered ${response.status}`);
|
|
133
|
+
const body = JSON.parse(
|
|
134
|
+
await boundedText(response.body, 32 * 1024 * 1024),
|
|
135
|
+
) as PiToolResponse;
|
|
136
|
+
if (!body.ok) throw new Error(body.error ?? "Host tool failed");
|
|
137
|
+
if (body.image) {
|
|
138
|
+
validateImages([body.image]);
|
|
139
|
+
saveToOutbox(
|
|
140
|
+
turn(),
|
|
141
|
+
"image",
|
|
142
|
+
Buffer.from(body.image.data, "base64"),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return toolText(body.text ?? "");
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Use public core Tool contributions inside Pi without copying their implementations. */
|
|
152
|
+
export function contributionExtension(
|
|
153
|
+
contributions: readonly ToolContribution[],
|
|
154
|
+
context: SessionContext,
|
|
155
|
+
turn: () => PiTurnContext,
|
|
156
|
+
adapt?: (
|
|
157
|
+
name: string,
|
|
158
|
+
input: Record<string, unknown>,
|
|
159
|
+
) => Record<string, unknown>,
|
|
160
|
+
): ExtensionFactory {
|
|
161
|
+
return (pi) => {
|
|
162
|
+
for (const contribution of contributions) {
|
|
163
|
+
const factory = contribution.session.snapshot().factory(context);
|
|
164
|
+
if (!factory) continue;
|
|
165
|
+
const proxy = new Proxy(pi, {
|
|
166
|
+
get(target, prop, receiver) {
|
|
167
|
+
if (prop !== "registerTool")
|
|
168
|
+
return Reflect.get(target, prop, receiver);
|
|
169
|
+
return (tool: Parameters<typeof pi.registerTool>[0]) => {
|
|
170
|
+
pi.registerTool({
|
|
171
|
+
...tool,
|
|
172
|
+
execute: async (id, params, signal, update, ctx, ...rest) => {
|
|
173
|
+
let toolResult:
|
|
174
|
+
| Awaited<ReturnType<typeof tool.execute>>
|
|
175
|
+
| undefined;
|
|
176
|
+
const result = await withReplyFiles(true, async () => {
|
|
177
|
+
toolResult = await tool.execute(
|
|
178
|
+
id,
|
|
179
|
+
adapt
|
|
180
|
+
? adapt(tool.name, params as Record<string, unknown>)
|
|
181
|
+
: params,
|
|
182
|
+
signal,
|
|
183
|
+
update,
|
|
184
|
+
ctx,
|
|
185
|
+
...rest,
|
|
186
|
+
);
|
|
187
|
+
return { ok: true, text: "" };
|
|
188
|
+
});
|
|
189
|
+
if (result.ok)
|
|
190
|
+
for (const file of result.files ?? [])
|
|
191
|
+
writeReplyFile(turn(), file);
|
|
192
|
+
if (!toolResult)
|
|
193
|
+
throw new Error("Contribution produced no tool result");
|
|
194
|
+
return toolResult;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
void factory(proxy);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function writeReplyFile(turn: PiTurnContext, file: ReplyFile): void {
|
|
205
|
+
if (!safeFileName(file.name)) throw new Error("Invalid reply filename");
|
|
206
|
+
const ext = extname(file.name).slice(1) || "bin";
|
|
207
|
+
const stem = extname(file.name)
|
|
208
|
+
? file.name.slice(0, -(ext.length + 1))
|
|
209
|
+
: file.name;
|
|
210
|
+
saveToOutbox(turn, stem, file.data, ext);
|
|
211
|
+
}
|