@springbrand/agent-runtime 0.1.3-alpha.4 → 0.1.3-alpha.6
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/package.json +11 -3
- package/src/adapter/cloudflare/index.ts +55 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +74 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/index.ts +49 -7
- package/src/kernel/bindings.ts +6 -6
- package/src/kernel/recoverable-chat-agent.ts +12 -0
- package/src/kernel/runtime-load.ts +89 -0
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/lib/mcp.ts +7 -3
- package/src/pi/assembly/context.ts +3 -3
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +1 -1
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +4 -10
- package/src/pi/runtime-adapter/index.ts +6 -2
- package/src/pi/tool/base.ts +17 -2
- package/src/pi/tool/compiler.ts +0 -1
- package/src/pi/tool/core.ts +13 -3
- package/src/pi/tool/mcp.ts +3 -4
- package/src/pi/tool/schedule.ts +11 -1
- package/src/pi/tool/skill.ts +55 -41
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +0 -1
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-sandbox.ts +15 -7
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +442 -328
- package/src/runtime-assembler.ts +255 -103
- package/src/runtime-definition.ts +173 -0
- package/src/runtime.ts +185 -25
- package/src/tool-registry.ts +143 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkspaceAdminPort,
|
|
3
|
+
WorkspaceFileInfo,
|
|
4
|
+
WorkspacePort,
|
|
5
|
+
WorkspaceQuota,
|
|
6
|
+
WorkspaceUsage,
|
|
7
|
+
} from "../../../kernel/bindings";
|
|
8
|
+
import type {
|
|
9
|
+
WorkspaceConditionalWriteResult,
|
|
10
|
+
WorkspaceFileVersion,
|
|
11
|
+
} from "./publisher";
|
|
12
|
+
|
|
13
|
+
const USAGE_PAGE_SIZE = 256;
|
|
14
|
+
const UNCONFIGURED_QUOTA: WorkspaceQuota = Object.freeze({
|
|
15
|
+
maxFiles: null,
|
|
16
|
+
maxTotalBytes: null,
|
|
17
|
+
maxFileBytes: null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function normalizePath(path: string): string {
|
|
21
|
+
const value = path.trim().replaceAll("\\", "/");
|
|
22
|
+
const absolute = value.startsWith("/") ? value : `/${value}`;
|
|
23
|
+
const parts = absolute.split("/").filter(Boolean);
|
|
24
|
+
if (parts.some((part) => part === ".." || part === ".")) {
|
|
25
|
+
throw new Error("workspace path traversal is not allowed");
|
|
26
|
+
}
|
|
27
|
+
return `/${parts.join("/")}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isReservedMemoryPath(path: string): boolean {
|
|
31
|
+
return path === "/shared/memories" || path.startsWith("/shared/memories/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A Session sees the user's shared mount plus its own directory. All other
|
|
36
|
+
* paths are interpreted relative to the current Session directory; another
|
|
37
|
+
* Session can never be named through this port. The Inbox Host also uses this
|
|
38
|
+
* object to create and remove the Session tree, so callers never need to build
|
|
39
|
+
* the physical `/sessions/<id>` path themselves.
|
|
40
|
+
*/
|
|
41
|
+
export class ScopedWorkspace implements WorkspacePort, WorkspaceAdminPort {
|
|
42
|
+
readonly sessionRoot: string;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private readonly parent: WorkspacePort,
|
|
46
|
+
readonly sessionId: string,
|
|
47
|
+
private readonly quota: WorkspaceQuota = UNCONFIGURED_QUOTA,
|
|
48
|
+
) {
|
|
49
|
+
if (!sessionId || sessionId.includes("/") || sessionId === "." || sessionId === "..") {
|
|
50
|
+
throw new Error("invalid session id");
|
|
51
|
+
}
|
|
52
|
+
this.sessionRoot = `/sessions/${sessionId}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async ensure(): Promise<void> {
|
|
56
|
+
await this.parent.mkdir("/shared", { recursive: true });
|
|
57
|
+
await this.parent.mkdir(this.sessionRoot, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Count only this Session's private tree. User-level `/shared` belongs to the
|
|
62
|
+
* Inbox and must not be charged once per Chat.
|
|
63
|
+
*/
|
|
64
|
+
async getUsage(): Promise<WorkspaceUsage> {
|
|
65
|
+
if (!(await this.parent.exists(this.sessionRoot))) {
|
|
66
|
+
return { fileCount: 0, directoryCount: 0, totalBytes: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let fileCount = 0;
|
|
70
|
+
let directoryCount = 0;
|
|
71
|
+
let totalBytes = 0;
|
|
72
|
+
const pending = [this.sessionRoot];
|
|
73
|
+
|
|
74
|
+
while (pending.length > 0) {
|
|
75
|
+
const directory = pending.pop()!;
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (true) {
|
|
78
|
+
const entries = await this.parent.readDir(directory, {
|
|
79
|
+
limit: USAGE_PAGE_SIZE,
|
|
80
|
+
offset,
|
|
81
|
+
});
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.type === "directory") {
|
|
84
|
+
directoryCount += 1;
|
|
85
|
+
pending.push(entry.path);
|
|
86
|
+
} else {
|
|
87
|
+
fileCount += 1;
|
|
88
|
+
totalBytes += entry.size;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (entries.length < USAGE_PAGE_SIZE) break;
|
|
92
|
+
offset += entries.length;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { fileCount, directoryCount, totalBytes };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getQuota(): Promise<WorkspaceQuota> {
|
|
100
|
+
return { ...this.quota };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Host-only lifecycle operation. It never removes the user-level `/shared` tree. */
|
|
104
|
+
async removeAll(): Promise<void> {
|
|
105
|
+
await this.parent.rm(this.sessionRoot, {
|
|
106
|
+
recursive: true,
|
|
107
|
+
force: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private physical(path: string): string {
|
|
112
|
+
const normalized = normalizePath(path);
|
|
113
|
+
if (normalized === "/shared" || normalized.startsWith("/shared/")) {
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
if (normalized === this.sessionRoot || normalized.startsWith(`${this.sessionRoot}/`)) {
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
if (normalized === "/sessions" || normalized.startsWith("/sessions/")) {
|
|
120
|
+
throw new Error("another Session workspace is not accessible");
|
|
121
|
+
}
|
|
122
|
+
return normalized === "/"
|
|
123
|
+
? this.sessionRoot
|
|
124
|
+
: `${this.sessionRoot}${normalized}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private writablePhysical(path: string): string {
|
|
128
|
+
const physical = this.physical(path);
|
|
129
|
+
if (isReservedMemoryPath(physical)) {
|
|
130
|
+
throw new Error("/shared/memories is read-only in Session workspaces");
|
|
131
|
+
}
|
|
132
|
+
return physical;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async assertNoSymlinkComponents(path: string): Promise<void> {
|
|
136
|
+
let root: string;
|
|
137
|
+
if (path === "/shared" || path.startsWith("/shared/")) {
|
|
138
|
+
root = "/shared";
|
|
139
|
+
} else if (
|
|
140
|
+
path === this.sessionRoot ||
|
|
141
|
+
path.startsWith(`${this.sessionRoot}/`)
|
|
142
|
+
) {
|
|
143
|
+
root = this.sessionRoot;
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error("another Session workspace is not accessible");
|
|
146
|
+
}
|
|
147
|
+
const relative = path.slice(root.length);
|
|
148
|
+
const parts = relative.split("/").filter(Boolean);
|
|
149
|
+
let current = root;
|
|
150
|
+
|
|
151
|
+
for (const part of ["", ...parts]) {
|
|
152
|
+
if (part) current = `${current}/${part}`;
|
|
153
|
+
const info = await this.parent.lstat(current);
|
|
154
|
+
if (!info) return;
|
|
155
|
+
if (info.type === "symlink") {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"symbolic links are not accessible in scoped workspaces",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async guardedPhysical(
|
|
164
|
+
path: string,
|
|
165
|
+
writable = false,
|
|
166
|
+
): Promise<string> {
|
|
167
|
+
const physical = writable
|
|
168
|
+
? this.writablePhysical(path)
|
|
169
|
+
: this.physical(path);
|
|
170
|
+
await this.assertNoSymlinkComponents(physical);
|
|
171
|
+
return physical;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private virtual(path: string): string {
|
|
175
|
+
if (path === this.sessionRoot) return "/";
|
|
176
|
+
if (path.startsWith(`${this.sessionRoot}/`)) {
|
|
177
|
+
return path.slice(this.sessionRoot.length);
|
|
178
|
+
}
|
|
179
|
+
return path;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private project(info: WorkspaceFileInfo | null): WorkspaceFileInfo | null {
|
|
183
|
+
return info ? { ...info, path: this.virtual(info.path) } : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async readFile(path: string) {
|
|
187
|
+
return this.parent.readFile(await this.guardedPhysical(path));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async readFileBytes(path: string) {
|
|
191
|
+
return this.parent.readFileBytes(await this.guardedPhysical(path));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async writeFile(path: string, content: string, mimeType?: string) {
|
|
195
|
+
return this.parent.writeFile(
|
|
196
|
+
await this.guardedPhysical(path, true),
|
|
197
|
+
content,
|
|
198
|
+
mimeType,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async writeFileBytes(
|
|
203
|
+
path: string,
|
|
204
|
+
data: Uint8Array | ArrayBuffer,
|
|
205
|
+
mimeType?: string,
|
|
206
|
+
) {
|
|
207
|
+
return this.parent.writeFileBytes(
|
|
208
|
+
await this.guardedPhysical(path, true),
|
|
209
|
+
data,
|
|
210
|
+
mimeType,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async writeFileBytesIfUnchanged(
|
|
215
|
+
path: string,
|
|
216
|
+
data: Uint8Array,
|
|
217
|
+
mimeType: string,
|
|
218
|
+
expected: WorkspaceFileVersion | null,
|
|
219
|
+
): Promise<WorkspaceConditionalWriteResult> {
|
|
220
|
+
const physical = await this.guardedPhysical(path, true);
|
|
221
|
+
if (physical === "/shared" || physical.startsWith("/shared/")) {
|
|
222
|
+
throw new Error("/shared is read-only for Sandbox publishing");
|
|
223
|
+
}
|
|
224
|
+
const current = await this.parent.stat(physical);
|
|
225
|
+
const currentVersion =
|
|
226
|
+
current?.type === "file"
|
|
227
|
+
? { updatedAt: current.updatedAt, size: current.size }
|
|
228
|
+
: null;
|
|
229
|
+
const matches =
|
|
230
|
+
expected === null
|
|
231
|
+
? current === null
|
|
232
|
+
: current?.type === "file" &&
|
|
233
|
+
current.updatedAt === expected.updatedAt &&
|
|
234
|
+
current.size === expected.size;
|
|
235
|
+
if (!matches) {
|
|
236
|
+
return {
|
|
237
|
+
written: false,
|
|
238
|
+
reason: "conflict",
|
|
239
|
+
current: currentVersion,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
await this.parent.writeFileBytes(physical, data, mimeType);
|
|
243
|
+
const written = await this.parent.stat(physical);
|
|
244
|
+
if (!written || written.type !== "file") {
|
|
245
|
+
throw new Error("published Workspace file could not be verified");
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
written: true,
|
|
249
|
+
version: {
|
|
250
|
+
updatedAt: written.updatedAt,
|
|
251
|
+
size: written.size,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async appendFile(path: string, content: string, mimeType?: string) {
|
|
257
|
+
return this.parent.appendFile(
|
|
258
|
+
await this.guardedPhysical(path, true),
|
|
259
|
+
content,
|
|
260
|
+
mimeType,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async exists(path: string) {
|
|
265
|
+
return this.parent.exists(await this.guardedPhysical(path));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async stat(path: string) {
|
|
269
|
+
return this.project(
|
|
270
|
+
await this.parent.stat(await this.guardedPhysical(path)),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async lstat(path: string) {
|
|
275
|
+
return this.project(
|
|
276
|
+
await this.parent.lstat(await this.guardedPhysical(path)),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async mkdir(path: string, opts?: { recursive?: boolean }) {
|
|
281
|
+
return this.parent.mkdir(await this.guardedPhysical(path, true), opts);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async readDir(dir = "/", opts?: { limit?: number; offset?: number }) {
|
|
285
|
+
const files = await this.parent.readDir(
|
|
286
|
+
await this.guardedPhysical(dir),
|
|
287
|
+
opts,
|
|
288
|
+
);
|
|
289
|
+
return files.map((file) => this.project(file)!);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async rm(path: string, opts?: { recursive?: boolean; force?: boolean }) {
|
|
293
|
+
const physical = await this.guardedPhysical(path, true);
|
|
294
|
+
if (physical === "/shared" || physical === this.sessionRoot) {
|
|
295
|
+
throw new Error("workspace mount roots cannot be removed");
|
|
296
|
+
}
|
|
297
|
+
return this.parent.rm(physical, opts);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async cp(src: string, dest: string, opts?: { recursive?: boolean }) {
|
|
301
|
+
return this.parent.cp(
|
|
302
|
+
await this.guardedPhysical(src),
|
|
303
|
+
await this.guardedPhysical(dest, true),
|
|
304
|
+
opts,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async mv(src: string, dest: string, opts?: { recursive?: boolean }) {
|
|
309
|
+
return this.parent.mv(
|
|
310
|
+
await this.guardedPhysical(src, true),
|
|
311
|
+
await this.guardedPhysical(dest, true),
|
|
312
|
+
opts,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async symlink(target: string, linkPath: string) {
|
|
317
|
+
/*
|
|
318
|
+
* A symlink stored under `/shared` could target this Session's physical
|
|
319
|
+
* directory and then be followed by another Session. Path normalization
|
|
320
|
+
* cannot guard a later filesystem dereference, so scoped runtimes do not
|
|
321
|
+
* create symlinks at all.
|
|
322
|
+
*/
|
|
323
|
+
void target;
|
|
324
|
+
void linkPath;
|
|
325
|
+
throw new Error("symbolic links are disabled in scoped workspaces");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async readlink(path: string) {
|
|
329
|
+
return this.virtual(
|
|
330
|
+
await this.parent.readlink(await this.guardedPhysical(path)),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async glob(pattern: string) {
|
|
335
|
+
const files = await this.parent.glob(this.physical(pattern));
|
|
336
|
+
for (const file of files) {
|
|
337
|
+
await this.assertNoSymlinkComponents(file.path);
|
|
338
|
+
}
|
|
339
|
+
return files.map((file) => this.project(file)!);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Build a runtime capability object that contains data-plane methods only.
|
|
345
|
+
*
|
|
346
|
+
* A TypeScript annotation does not remove extra methods at runtime. Returning
|
|
347
|
+
* this explicit facade prevents Agent Runtime and Sandbox adapters from
|
|
348
|
+
* reaching ScopedWorkspace lifecycle/accounting methods through the same
|
|
349
|
+
* object reference.
|
|
350
|
+
*/
|
|
351
|
+
export function createWorkspacePortFacade(
|
|
352
|
+
workspace: WorkspacePort,
|
|
353
|
+
): WorkspacePort {
|
|
354
|
+
const facade: WorkspacePort = {
|
|
355
|
+
readFile: (path) => workspace.readFile(path),
|
|
356
|
+
readFileBytes: (path) => workspace.readFileBytes(path),
|
|
357
|
+
writeFile: (path, content, mimeType) =>
|
|
358
|
+
workspace.writeFile(path, content, mimeType),
|
|
359
|
+
writeFileBytes: (path, data, mimeType) =>
|
|
360
|
+
workspace.writeFileBytes(path, data, mimeType),
|
|
361
|
+
appendFile: (path, content, mimeType) =>
|
|
362
|
+
workspace.appendFile(path, content, mimeType),
|
|
363
|
+
exists: (path) => workspace.exists(path),
|
|
364
|
+
stat: (path) => workspace.stat(path),
|
|
365
|
+
lstat: (path) => workspace.lstat(path),
|
|
366
|
+
mkdir: (path, opts) => workspace.mkdir(path, opts),
|
|
367
|
+
readDir: (dir, opts) => workspace.readDir(dir, opts),
|
|
368
|
+
rm: (path, opts) => workspace.rm(path, opts),
|
|
369
|
+
cp: (src, dest, opts) => workspace.cp(src, dest, opts),
|
|
370
|
+
mv: (src, dest, opts) => workspace.mv(src, dest, opts),
|
|
371
|
+
symlink: (target, linkPath) => workspace.symlink(target, linkPath),
|
|
372
|
+
readlink: (path) => workspace.readlink(path),
|
|
373
|
+
glob: (pattern) => workspace.glob(pattern),
|
|
374
|
+
};
|
|
375
|
+
return Object.freeze(facade);
|
|
376
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolChildAdapter,
|
|
3
|
+
AgentToolRunInspection,
|
|
4
|
+
AgentToolStoredChunk,
|
|
5
|
+
} from "agents";
|
|
6
|
+
import { AgentRuntimeKernel } from "./runtime";
|
|
7
|
+
|
|
8
|
+
const AGENT_TOOL_RUN_PREFIX = "universal-agent:temporary-agent-run:";
|
|
9
|
+
|
|
10
|
+
interface AgentToolRunRecord {
|
|
11
|
+
runId: string;
|
|
12
|
+
submissionId: string;
|
|
13
|
+
startedAt: number;
|
|
14
|
+
completedAt?: number;
|
|
15
|
+
summary?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function agentToolRunKey(runId: string): string {
|
|
19
|
+
return `${AGENT_TOOL_RUN_PREFIX}${runId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 把 Cloudflare Agent Tool 子运行协议适配到 Runtime Submission 生命周期。
|
|
24
|
+
*
|
|
25
|
+
* 具体 Runtime 只负责准备输入与清理资源;启动、查询、取消和结果读取统一留在这里。
|
|
26
|
+
*/
|
|
27
|
+
export abstract class AgentToolRuntimeKernel<
|
|
28
|
+
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
29
|
+
> extends AgentRuntimeKernel<Env>
|
|
30
|
+
implements AgentToolChildAdapter<unknown, string> {
|
|
31
|
+
protected abstract prepareAgentToolRun(input: unknown): Promise<string>;
|
|
32
|
+
protected abstract cleanupAgentToolRun(): Promise<void>;
|
|
33
|
+
|
|
34
|
+
async startAgentToolRun(
|
|
35
|
+
input: unknown,
|
|
36
|
+
options: { runId: string; signal?: AbortSignal },
|
|
37
|
+
): Promise<AgentToolRunInspection<string>> {
|
|
38
|
+
options.signal?.throwIfAborted();
|
|
39
|
+
try {
|
|
40
|
+
const prompt = await this.prepareAgentToolRun(input);
|
|
41
|
+
const receipt = await this.submitPrompt(prompt, {
|
|
42
|
+
idempotencyKey: options.runId,
|
|
43
|
+
});
|
|
44
|
+
const record: AgentToolRunRecord = {
|
|
45
|
+
runId: options.runId,
|
|
46
|
+
submissionId: receipt.submissionId,
|
|
47
|
+
startedAt: receipt.createdAt,
|
|
48
|
+
};
|
|
49
|
+
await this.ctx.storage.put(agentToolRunKey(options.runId), record);
|
|
50
|
+
const cancel = () => {
|
|
51
|
+
void this.cancelAgentToolRun(options.runId, options.signal?.reason);
|
|
52
|
+
};
|
|
53
|
+
if (options.signal?.aborted) cancel();
|
|
54
|
+
else options.signal?.addEventListener("abort", cancel, { once: true });
|
|
55
|
+
return {
|
|
56
|
+
runId: options.runId,
|
|
57
|
+
status: "running",
|
|
58
|
+
startedAt: record.startedAt,
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
await this.cleanupAgentToolRun();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async inspectAgentToolRun(
|
|
67
|
+
runId: string,
|
|
68
|
+
): Promise<AgentToolRunInspection<string> | null> {
|
|
69
|
+
const key = agentToolRunKey(runId);
|
|
70
|
+
const record = await this.ctx.storage.get<AgentToolRunRecord>(key);
|
|
71
|
+
if (!record) return null;
|
|
72
|
+
const receipt = await this.getSubmission(record.submissionId);
|
|
73
|
+
if (!receipt) return null;
|
|
74
|
+
if (receipt.status === "pending" || receipt.status === "running") {
|
|
75
|
+
return { runId, status: "running", startedAt: record.startedAt };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const completedAt = receipt.completedAt ?? Date.now();
|
|
79
|
+
if (receipt.status === "completed") {
|
|
80
|
+
const summary = record.summary ?? await this.latestAssistantText();
|
|
81
|
+
if (!summary) {
|
|
82
|
+
await this.cleanupAgentToolRun();
|
|
83
|
+
return {
|
|
84
|
+
runId,
|
|
85
|
+
status: "error",
|
|
86
|
+
error: "Temporary Agent completed without final text",
|
|
87
|
+
startedAt: record.startedAt,
|
|
88
|
+
completedAt,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
await this.ctx.storage.put(key, {
|
|
92
|
+
...record,
|
|
93
|
+
summary,
|
|
94
|
+
completedAt,
|
|
95
|
+
} satisfies AgentToolRunRecord);
|
|
96
|
+
await this.cleanupAgentToolRun();
|
|
97
|
+
return {
|
|
98
|
+
runId,
|
|
99
|
+
status: "completed",
|
|
100
|
+
output: summary,
|
|
101
|
+
summary,
|
|
102
|
+
startedAt: record.startedAt,
|
|
103
|
+
completedAt,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await this.cleanupAgentToolRun();
|
|
108
|
+
return receipt.status === "aborted"
|
|
109
|
+
? {
|
|
110
|
+
runId,
|
|
111
|
+
status: "aborted",
|
|
112
|
+
error: receipt.error ?? "Temporary Agent was cancelled",
|
|
113
|
+
startedAt: record.startedAt,
|
|
114
|
+
completedAt,
|
|
115
|
+
}
|
|
116
|
+
: {
|
|
117
|
+
runId,
|
|
118
|
+
status: "error",
|
|
119
|
+
error: receipt.error ??
|
|
120
|
+
(receipt.status === "skipped"
|
|
121
|
+
? "Temporary Agent submission was skipped"
|
|
122
|
+
: "Temporary Agent failed"),
|
|
123
|
+
startedAt: record.startedAt,
|
|
124
|
+
completedAt,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getAgentToolChunks(
|
|
129
|
+
_runId: string,
|
|
130
|
+
_options?: { afterSequence?: number },
|
|
131
|
+
): Promise<AgentToolStoredChunk[]> {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async cancelAgentToolRun(runId: string, reason?: unknown): Promise<void> {
|
|
136
|
+
const record = await this.ctx.storage.get<AgentToolRunRecord>(
|
|
137
|
+
agentToolRunKey(runId),
|
|
138
|
+
);
|
|
139
|
+
try {
|
|
140
|
+
if (record) {
|
|
141
|
+
await this.cancelSubmissionById(
|
|
142
|
+
record.submissionId,
|
|
143
|
+
reason instanceof Error
|
|
144
|
+
? reason.message
|
|
145
|
+
: String(reason ?? "Temporary Agent cancelled"),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
await this.cleanupAgentToolRun();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* - `SubAgent` 是由父 Agent 发起、但拥有独立执行上下文的子任务执行者。
|
|
23
23
|
* - `canonical transcript` 是 Pi 持久化和恢复 Turn 时使用的权威消息历史。
|
|
24
24
|
* - `admission` 是提示进入 Turn 前的接纳、去重和状态登记流程。
|
|
25
|
-
* - `
|
|
26
|
-
* - `
|
|
27
|
-
* - `
|
|
25
|
+
* - `defineRuntimeAgent.load` 返回 config 和已加载 Resource。
|
|
26
|
+
* - `defineRuntimeAgent.tools` 是 Host 第一层 Tool 注册表声明。
|
|
27
|
+
* - `assembleRuntimeSnapshot` 消费该输入与 hooks,产出候选 Snapshot。
|
|
28
|
+
* - `Port` 是 Runtime 调用外部能力时依赖的最小接口(Definition 作者不直接装配)。
|
|
28
29
|
* - `RuntimeBindings` 是本次装配选中的 Port 和可执行对象集合。
|
|
29
30
|
* - `RuntimeBuilder` 是包内实现,用来校验贡献并生成候选结果。
|
|
30
31
|
* - `RuntimeCandidate` 是尚未生效的候选结果和提交前检查。
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
*
|
|
37
38
|
* Snapshot 不允许替换已选集合,但不承诺把每个 Port 的内部对象深冻结。
|
|
38
39
|
*
|
|
39
|
-
* 正常调用顺序是 `
|
|
40
|
+
* 正常调用顺序是 `assemble → validate → freeze → guard → commit`。
|
|
40
41
|
* Runtime 独占跨能力校验和原子提交。
|
|
41
42
|
*
|
|
42
43
|
* @packageDocumentation
|
|
@@ -46,6 +47,7 @@ export * from "./kernel/extensions";
|
|
|
46
47
|
export * from "./kernel/profile";
|
|
47
48
|
export * from "./kernel/receipts";
|
|
48
49
|
export * from "./kernel/runtime-config";
|
|
50
|
+
export type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
|
|
49
51
|
export * from "./runtime-agent";
|
|
50
52
|
export * from "./kernel/state";
|
|
51
53
|
export * from "./lib/execution-level";
|
|
@@ -57,6 +59,31 @@ export type {
|
|
|
57
59
|
RuntimeSkillContribution,
|
|
58
60
|
RuntimeToolSurfacePolicy,
|
|
59
61
|
} from "./runtime-assembler";
|
|
62
|
+
export { assembleRuntimeSnapshot, toolRegistryPiToolCandidates } from "./runtime-assembler";
|
|
63
|
+
export type {
|
|
64
|
+
PlatformToolContext,
|
|
65
|
+
PlatformToolRegistry,
|
|
66
|
+
PlatformToolSpec,
|
|
67
|
+
ProfileOverrides,
|
|
68
|
+
ResolvedConnectors,
|
|
69
|
+
ResolvedResources,
|
|
70
|
+
RuntimeAgentContext,
|
|
71
|
+
RuntimeAgentHooks,
|
|
72
|
+
RuntimeSettings,
|
|
73
|
+
RuntimeToolBindings,
|
|
74
|
+
ToolAssemblyResult,
|
|
75
|
+
ToolContext,
|
|
76
|
+
ToolRegistry,
|
|
77
|
+
ToolSpec,
|
|
78
|
+
} from "./runtime-definition";
|
|
79
|
+
export {
|
|
80
|
+
emptyPlatformToolRegistry,
|
|
81
|
+
emptyToolRegistry,
|
|
82
|
+
mergeToolRegistries,
|
|
83
|
+
normalizeToolAssembly,
|
|
84
|
+
piCandidateToToolSpec,
|
|
85
|
+
toolRegistryFromPiCandidates,
|
|
86
|
+
} from "./tool-registry";
|
|
60
87
|
export type { ModelOption } from "./lib/model-catalog";
|
|
61
88
|
export {
|
|
62
89
|
assembleSubagentPrompt,
|
|
@@ -82,16 +109,30 @@ export type {
|
|
|
82
109
|
SettledPiToolCall,
|
|
83
110
|
} from "./pi/tool";
|
|
84
111
|
export { basePiToolCandidates } from "./pi/tool";
|
|
85
|
-
export { schedulePiToolCandidates } from "./pi/tool";
|
|
86
112
|
export {
|
|
113
|
+
createMemoryTools,
|
|
114
|
+
memoryPiToolCandidate,
|
|
115
|
+
} from "./pi/tool";
|
|
116
|
+
export {
|
|
117
|
+
createScheduleTools,
|
|
118
|
+
schedulePiToolCandidates,
|
|
119
|
+
} from "./pi/tool";
|
|
120
|
+
export {
|
|
121
|
+
createSandboxTools,
|
|
122
|
+
createWorkspaceTools,
|
|
87
123
|
sandboxPiToolCandidates,
|
|
88
124
|
workspacePiToolCandidates,
|
|
89
125
|
} from "./pi/tool";
|
|
90
|
-
export {
|
|
126
|
+
export {
|
|
127
|
+
createSubagentTools,
|
|
128
|
+
subagentPiToolCandidates,
|
|
129
|
+
} from "./pi/tool";
|
|
91
130
|
export { skillPiToolCandidates } from "./pi/tool";
|
|
92
131
|
export type { PiSkillBinding } from "./pi/tool";
|
|
93
132
|
export {
|
|
94
133
|
browserQuickActionPiToolCandidates,
|
|
134
|
+
createCodeExecutionTool,
|
|
135
|
+
codeExecutionPiToolCandidate,
|
|
95
136
|
} from "./pi/tool";
|
|
96
137
|
export {
|
|
97
138
|
createWorkspaceCodeExecutionPort,
|
|
@@ -103,10 +144,11 @@ export {
|
|
|
103
144
|
export type {
|
|
104
145
|
TemporaryAgentApprovalDecision,
|
|
105
146
|
TemporaryAgentApprovalRequest,
|
|
106
|
-
|
|
147
|
+
TemporaryAgentLaunch,
|
|
107
148
|
TemporaryAgentRequest,
|
|
108
149
|
TemporaryAgentRunContext,
|
|
109
150
|
} from "./layers/orchestration/temporary-agent/core";
|
|
151
|
+
export { TEMPORARY_AGENT_LAUNCH_KEY } from "./layers/orchestration/temporary-agent/core";
|
|
110
152
|
export {
|
|
111
153
|
temporaryAgentExtensionIsSafe,
|
|
112
154
|
temporaryAgentToolAllowed,
|
package/src/kernel/bindings.ts
CHANGED
|
@@ -893,15 +893,15 @@ export interface RuntimePlatformPort {
|
|
|
893
893
|
}
|
|
894
894
|
|
|
895
895
|
/**
|
|
896
|
-
*
|
|
896
|
+
* 汇总本次装配已配置的 Skill 来源。
|
|
897
897
|
*
|
|
898
898
|
* @remarks
|
|
899
899
|
* Builder 在生成候选 Snapshot 时创建,Pi 组装在生成 Skill 工具时读取。
|
|
900
900
|
*
|
|
901
|
-
* catalog
|
|
901
|
+
* catalog、来源与脚本策略一起保留,避免装配期重新访问来源。
|
|
902
902
|
*/
|
|
903
903
|
export interface RuntimeSkillBindings {
|
|
904
|
-
/**
|
|
904
|
+
/** Configured external sources and their isolated script policy. */
|
|
905
905
|
sources: readonly RuntimeSkillSourceBinding[];
|
|
906
906
|
}
|
|
907
907
|
|
|
@@ -911,7 +911,7 @@ export interface RuntimeSkillBindings {
|
|
|
911
911
|
* @remarks
|
|
912
912
|
* Skill Plugin 在准备来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
|
|
913
913
|
*
|
|
914
|
-
*
|
|
914
|
+
* 这是 Config 已解析的执行策略,执行期不应自动放宽。
|
|
915
915
|
*/
|
|
916
916
|
export interface RuntimeSkillScriptPolicy {
|
|
917
917
|
network: "none" | "full";
|
|
@@ -920,12 +920,12 @@ export interface RuntimeSkillScriptPolicy {
|
|
|
920
920
|
}
|
|
921
921
|
|
|
922
922
|
/**
|
|
923
|
-
*
|
|
923
|
+
* 把一个已配置 Skill 来源与其脚本策略放在同一快照中。
|
|
924
924
|
*
|
|
925
925
|
* @remarks
|
|
926
926
|
* Builder 为每个 Skill 贡献创建,Pi 组装和 Skill 工具按名称查找并读取。
|
|
927
927
|
*
|
|
928
|
-
*
|
|
928
|
+
* 名称、内容来源与脚本策略不分开存放,脚本 runner 才能按 Skill 名找到对应策略。
|
|
929
929
|
*/
|
|
930
930
|
export interface RuntimeSkillSourceBinding {
|
|
931
931
|
name: string;
|
|
@@ -353,7 +353,13 @@ export abstract class RecoverableChatAgent<
|
|
|
353
353
|
* 清理延后执行,为断线重连的客户端保留终态流片段。
|
|
354
354
|
*/
|
|
355
355
|
protected completeRecoverableStream(streamId: string): void {
|
|
356
|
+
const completedRequestId = this.resumableStream.activeRequestId;
|
|
356
357
|
this.resumableStream.complete(streamId);
|
|
358
|
+
this.pendingResumeConnections.clear();
|
|
359
|
+
if (completedRequestId === this.continuation.activeRequestId) {
|
|
360
|
+
this.continuation.activeRequestId = null;
|
|
361
|
+
this.continuation.activeConnectionId = null;
|
|
362
|
+
}
|
|
357
363
|
void this.ensureStreamCleanupScheduled();
|
|
358
364
|
}
|
|
359
365
|
|
|
@@ -366,7 +372,13 @@ export abstract class RecoverableChatAgent<
|
|
|
366
372
|
* 流错误标志与业务失败分开,因为续传协议和提交生命周期的责任不同。
|
|
367
373
|
*/
|
|
368
374
|
protected failRecoverableStream(streamId: string): void {
|
|
375
|
+
const erroredRequestId = this.resumableStream.activeRequestId;
|
|
369
376
|
this.resumableStream.markError(streamId);
|
|
377
|
+
this.pendingResumeConnections.clear();
|
|
378
|
+
if (erroredRequestId === this.continuation.activeRequestId) {
|
|
379
|
+
this.continuation.activeRequestId = null;
|
|
380
|
+
this.continuation.activeConnectionId = null;
|
|
381
|
+
}
|
|
370
382
|
void this.ensureStreamCleanupScheduled();
|
|
371
383
|
}
|
|
372
384
|
|