@springbrand/agent-runtime 0.1.3-alpha.4 → 0.1.3-alpha.5
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 +3 -1
- package/src/adapter/cloudflare/index.ts +60 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +86 -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 +256 -0
- package/src/adapter/cloudflare/universal-agent/definition.ts +71 -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/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +4 -2
- package/src/pi/runtime-adapter/index.ts +6 -2
- package/src/pi/tool/base.ts +17 -0
- package/src/pi/tool/core.ts +13 -0
- package/src/pi/tool/schedule.ts +11 -0
- package/src/pi/tool/subagent.ts +14 -0
- package/src/pi/tool/workspace-sandbox.ts +15 -0
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +429 -315
- package/src/runtime-assembler.ts +249 -99
- package/src/runtime-definition.ts +173 -0
- package/src/runtime.ts +139 -12
- 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,
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
import type { ExecutionLevel } from "../../../lib/execution-level";
|
|
2
|
+
|
|
3
|
+
export const TEMPORARY_AGENT_LAUNCH_KEY =
|
|
4
|
+
"universal-agent:temporary-agent-launch";
|
|
5
|
+
|
|
1
6
|
export interface TemporaryAgentRequest {
|
|
2
7
|
subagentName: string;
|
|
3
8
|
instructions: string;
|
|
4
9
|
task: string;
|
|
5
10
|
}
|
|
6
11
|
|
|
12
|
+
export interface TemporaryAgentLaunch<Config = unknown>
|
|
13
|
+
extends TemporaryAgentRequest {
|
|
14
|
+
runtimeKey: string;
|
|
15
|
+
config: Config;
|
|
16
|
+
executionLevel: ExecutionLevel;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export interface TemporaryAgentRunContext {
|
|
8
20
|
signal: AbortSignal;
|
|
9
21
|
requestId: string;
|
|
@@ -149,4 +161,3 @@ export class TemporaryAgentCoordinator {
|
|
|
149
161
|
});
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
|
-
import type { ExecutionLevel } from "../../../lib/execution-level";
|
|
@@ -10,7 +10,6 @@ const BLOCKED_TOOLS = new Set([
|
|
|
10
10
|
"resume_schedule",
|
|
11
11
|
"change_schedule_agent",
|
|
12
12
|
"cancel_schedule",
|
|
13
|
-
"execute",
|
|
14
13
|
"set_context",
|
|
15
14
|
"load_context",
|
|
16
15
|
"search_context",
|
|
@@ -25,7 +24,7 @@ const BLOCKED_TOOLS = new Set([
|
|
|
25
24
|
* @remarks
|
|
26
25
|
* 宿主在装配临时 Agent 的平台、Workspace、Sandbox 和其他 Tool 时调用;调用方可再加名字或前缀黑名单。
|
|
27
26
|
*
|
|
28
|
-
*
|
|
27
|
+
* 固定黑名单排除再次委派、调度和主 Session 上下文,以保持单层临时 Agent 和已确认的继承范围。
|
|
29
28
|
*
|
|
30
29
|
* Agent、Session、Workspace 和 Tool 的术语见 `src/index.ts`。
|
|
31
30
|
*/
|
|
@@ -2,6 +2,13 @@ import type { UIMessage } from "ai";
|
|
|
2
2
|
|
|
3
3
|
export type UIChatTrigger = "submit-message" | "regenerate-message";
|
|
4
4
|
|
|
5
|
+
/** A user-selected Runtime capability persisted with the visible UIMessage. */
|
|
6
|
+
export interface RequestedCapability {
|
|
7
|
+
readonly kind: "skill" | "plan";
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly label: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface UIChatRequestBody {
|
|
6
13
|
readonly messages: readonly UIMessage[];
|
|
7
14
|
readonly trigger: UIChatTrigger;
|