@cloudflare/think 0.0.0 → 0.0.2
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/README.md +241 -0
- package/dist/classPrivateFieldSet2-COLddhya.js +27 -0
- package/dist/classPrivateMethodInitSpec-CdQXQy1O.js +7 -0
- package/dist/extensions/index.d.ts +20 -0
- package/dist/extensions/index.js +62 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/index-BlcvIdWK.d.ts +171 -0
- package/dist/index-C4OTSwUW.d.ts +193 -0
- package/dist/manager-DIV0gQf3.js +214 -0
- package/dist/manager-DIV0gQf3.js.map +1 -0
- package/dist/message-builder.d.ts +51 -0
- package/dist/message-builder.js +217 -0
- package/dist/message-builder.js.map +1 -0
- package/dist/session/index.d.ts +22 -0
- package/dist/session/index.js +2 -0
- package/dist/session-C6ZU_1zM.js +507 -0
- package/dist/session-C6ZU_1zM.js.map +1 -0
- package/dist/think.d.ts +315 -0
- package/dist/think.js +701 -0
- package/dist/think.js.map +1 -0
- package/dist/tools/execute.d.ts +105 -0
- package/dist/tools/execute.js +64 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/extensions.d.ts +67 -0
- package/dist/tools/extensions.js +85 -0
- package/dist/tools/extensions.js.map +1 -0
- package/dist/tools/workspace.d.ts +303 -0
- package/dist/tools/workspace.js +398 -0
- package/dist/tools/workspace.js.map +1 -0
- package/dist/transport.d.ts +69 -0
- package/dist/transport.js +166 -0
- package/dist/transport.js.map +1 -0
- package/package.json +83 -9
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { UIMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
//#region src/session/storage.d.ts
|
|
4
|
+
interface Session {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
updated_at: string;
|
|
9
|
+
}
|
|
10
|
+
interface Compaction {
|
|
11
|
+
id: string;
|
|
12
|
+
session_id: string;
|
|
13
|
+
summary: string;
|
|
14
|
+
from_message_id: string;
|
|
15
|
+
to_message_id: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/session/index.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Truncate from the head (keep the end of the content).
|
|
22
|
+
*/
|
|
23
|
+
declare function truncateHead(text: string, maxChars?: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Truncate from the tail (keep the start of the content).
|
|
26
|
+
*/
|
|
27
|
+
declare function truncateTail(text: string, maxChars?: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Truncate by line count (keep the first N lines).
|
|
30
|
+
*/
|
|
31
|
+
declare function truncateLines(text: string, maxLines?: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* Truncate from both ends, keeping the start and end.
|
|
34
|
+
*/
|
|
35
|
+
declare function truncateMiddle(text: string, maxChars?: number): string;
|
|
36
|
+
/**
|
|
37
|
+
* Smart truncation for tool output.
|
|
38
|
+
*/
|
|
39
|
+
declare function truncateToolOutput(
|
|
40
|
+
output: string,
|
|
41
|
+
options?: {
|
|
42
|
+
maxChars?: number;
|
|
43
|
+
maxLines?: number;
|
|
44
|
+
strategy?: "head" | "tail" | "middle";
|
|
45
|
+
}
|
|
46
|
+
): string;
|
|
47
|
+
interface AgentLike {
|
|
48
|
+
sql: (
|
|
49
|
+
strings: TemplateStringsArray,
|
|
50
|
+
...values: (string | number | boolean | null)[]
|
|
51
|
+
) => Array<Record<string, unknown>>;
|
|
52
|
+
}
|
|
53
|
+
interface SessionManagerOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Maximum number of messages on the current branch before
|
|
56
|
+
* needsCompaction() returns true. Default: 100.
|
|
57
|
+
*/
|
|
58
|
+
maxContextMessages?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Raw SQL exec function for batch operations (e.g. DELETE ... WHERE id IN (...)).
|
|
61
|
+
* When provided, batch deletes use a single query instead of N individual ones.
|
|
62
|
+
*
|
|
63
|
+
* Typically: `(query, ...values) => { agent.ctx.storage.sql.exec(query, ...values); }`
|
|
64
|
+
*/
|
|
65
|
+
exec?: (
|
|
66
|
+
query: string,
|
|
67
|
+
...values: (string | number | boolean | null)[]
|
|
68
|
+
) => void;
|
|
69
|
+
}
|
|
70
|
+
declare class SessionManager {
|
|
71
|
+
private _storage;
|
|
72
|
+
private _options;
|
|
73
|
+
constructor(agent: AgentLike, options?: SessionManagerOptions);
|
|
74
|
+
/**
|
|
75
|
+
* Create a new session with a name.
|
|
76
|
+
*/
|
|
77
|
+
create(name: string): Session;
|
|
78
|
+
/**
|
|
79
|
+
* Get a session by ID.
|
|
80
|
+
*/
|
|
81
|
+
get(sessionId: string): Session | null;
|
|
82
|
+
/**
|
|
83
|
+
* List all sessions, most recently updated first.
|
|
84
|
+
*/
|
|
85
|
+
list(): Session[];
|
|
86
|
+
/**
|
|
87
|
+
* Delete a session and all its messages and compactions.
|
|
88
|
+
*/
|
|
89
|
+
delete(sessionId: string): void;
|
|
90
|
+
/**
|
|
91
|
+
* Clear all messages and compactions for a session without
|
|
92
|
+
* deleting the session itself.
|
|
93
|
+
*/
|
|
94
|
+
clearMessages(sessionId: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Rename a session.
|
|
97
|
+
*/
|
|
98
|
+
rename(sessionId: string, name: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* Append a message to a session. If parentId is not provided,
|
|
101
|
+
* the message is appended after the latest leaf.
|
|
102
|
+
*
|
|
103
|
+
* Idempotent — appending the same message.id twice is a no-op.
|
|
104
|
+
*
|
|
105
|
+
* Returns the stored message ID.
|
|
106
|
+
*/
|
|
107
|
+
append(sessionId: string, message: UIMessage, parentId?: string): string;
|
|
108
|
+
/**
|
|
109
|
+
* Insert or update a message. First call inserts, subsequent calls
|
|
110
|
+
* update the content. Enables incremental persistence.
|
|
111
|
+
*
|
|
112
|
+
* Idempotent on insert, content-updating on subsequent calls.
|
|
113
|
+
*/
|
|
114
|
+
upsert(sessionId: string, message: UIMessage, parentId?: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* Delete a single message by ID.
|
|
117
|
+
* Children of the deleted message naturally become path roots
|
|
118
|
+
* (their parent_id points to a missing row, truncating the CTE walk).
|
|
119
|
+
*/
|
|
120
|
+
deleteMessage(messageId: string): void;
|
|
121
|
+
/**
|
|
122
|
+
* Delete multiple messages by ID.
|
|
123
|
+
*/
|
|
124
|
+
deleteMessages(messageIds: string[]): void;
|
|
125
|
+
/**
|
|
126
|
+
* Append multiple messages in sequence (each parented to the previous).
|
|
127
|
+
* Returns the ID of the last appended message.
|
|
128
|
+
*/
|
|
129
|
+
appendAll(
|
|
130
|
+
sessionId: string,
|
|
131
|
+
messages: UIMessage[],
|
|
132
|
+
parentId?: string
|
|
133
|
+
): string | null;
|
|
134
|
+
/**
|
|
135
|
+
* Get the conversation history for a session as UIMessage[].
|
|
136
|
+
*
|
|
137
|
+
* If leafId is provided, returns the path from root to that leaf
|
|
138
|
+
* (a specific branch). Otherwise returns the path to the most
|
|
139
|
+
* recent leaf (the "current" branch).
|
|
140
|
+
*
|
|
141
|
+
* If compactions exist, older messages covered by a compaction
|
|
142
|
+
* are replaced with a system message containing the summary.
|
|
143
|
+
*/
|
|
144
|
+
getHistory(sessionId: string, leafId?: string): UIMessage[];
|
|
145
|
+
/**
|
|
146
|
+
* Get the total message count for a session (across all branches).
|
|
147
|
+
*/
|
|
148
|
+
getMessageCount(sessionId: string): number;
|
|
149
|
+
/**
|
|
150
|
+
* Check if the session's current branch needs compaction.
|
|
151
|
+
* Uses a count-only query — does not load message content.
|
|
152
|
+
*/
|
|
153
|
+
needsCompaction(sessionId: string): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Get the children of a message (branches from that point).
|
|
156
|
+
*/
|
|
157
|
+
getBranches(messageId: string): UIMessage[];
|
|
158
|
+
/**
|
|
159
|
+
* Fork a session at a specific message, creating a new session
|
|
160
|
+
* with the history up to that point copied over.
|
|
161
|
+
*/
|
|
162
|
+
fork(atMessageId: string, newName: string): Session;
|
|
163
|
+
/**
|
|
164
|
+
* Add a compaction record. The summary replaces messages from
|
|
165
|
+
* fromMessageId to toMessageId in context assembly.
|
|
166
|
+
*
|
|
167
|
+
* Typically called after using an LLM to summarize older messages.
|
|
168
|
+
*/
|
|
169
|
+
addCompaction(
|
|
170
|
+
sessionId: string,
|
|
171
|
+
summary: string,
|
|
172
|
+
fromMessageId: string,
|
|
173
|
+
toMessageId: string
|
|
174
|
+
): Compaction;
|
|
175
|
+
/**
|
|
176
|
+
* Get all compaction records for a session.
|
|
177
|
+
*/
|
|
178
|
+
getCompactions(sessionId: string): Compaction[];
|
|
179
|
+
private _applyCompactions;
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export {
|
|
183
|
+
truncateMiddle as a,
|
|
184
|
+
Compaction as c,
|
|
185
|
+
truncateLines as i,
|
|
186
|
+
Session as l,
|
|
187
|
+
SessionManagerOptions as n,
|
|
188
|
+
truncateTail as o,
|
|
189
|
+
truncateHead as r,
|
|
190
|
+
truncateToolOutput as s,
|
|
191
|
+
SessionManager as t
|
|
192
|
+
};
|
|
193
|
+
//# sourceMappingURL=index-C4OTSwUW.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldGet2, r as _assertClassBrand, t as _classPrivateFieldSet2 } from "./classPrivateFieldSet2-COLddhya.js";
|
|
2
|
+
import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-CdQXQy1O.js";
|
|
3
|
+
import { jsonSchema, tool } from "ai";
|
|
4
|
+
//#region src/extensions/manager.ts
|
|
5
|
+
/**
|
|
6
|
+
* ExtensionManager — loads, manages, and exposes tools from extension Workers.
|
|
7
|
+
*
|
|
8
|
+
* Extensions are sandboxed Workers created via WorkerLoader. Each extension
|
|
9
|
+
* declares tools (with JSON Schema inputs) and permissions. The manager:
|
|
10
|
+
*
|
|
11
|
+
* 1. Wraps extension source in a Worker module with describe/execute RPC
|
|
12
|
+
* 2. Loads it via WorkerLoader with permission-gated bindings
|
|
13
|
+
* 3. Discovers tools via describe() RPC call
|
|
14
|
+
* 4. Exposes them as AI SDK tools via getTools()
|
|
15
|
+
*
|
|
16
|
+
* Extension source format — a JS object expression defining tools:
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* ({
|
|
20
|
+
* greet: {
|
|
21
|
+
* description: "Greet someone",
|
|
22
|
+
* parameters: { name: { type: "string" } },
|
|
23
|
+
* required: ["name"],
|
|
24
|
+
* execute: async (args, host) => `Hello, ${args.name}!`
|
|
25
|
+
* }
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* The `host` parameter in execute is provided via `env.host` — a loopback
|
|
30
|
+
* binding that resolves the parent agent and delegates workspace operations
|
|
31
|
+
* (gated by permissions). See HostBridgeLoopback.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Sanitize a name for use as a tool name prefix.
|
|
35
|
+
* Replaces any non-alphanumeric characters with underscores and
|
|
36
|
+
* collapses consecutive underscores.
|
|
37
|
+
*/
|
|
38
|
+
function sanitizeName(name) {
|
|
39
|
+
if (!name || name.trim().length === 0) throw new Error("Extension name must not be empty");
|
|
40
|
+
return name.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
41
|
+
}
|
|
42
|
+
const STORAGE_PREFIX = "ext:";
|
|
43
|
+
var _loader = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
var _storage = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
var _createHostBinding = /* @__PURE__ */ new WeakMap();
|
|
46
|
+
var _extensions = /* @__PURE__ */ new WeakMap();
|
|
47
|
+
var _restored = /* @__PURE__ */ new WeakMap();
|
|
48
|
+
var _ExtensionManager_brand = /* @__PURE__ */ new WeakSet();
|
|
49
|
+
var ExtensionManager = class {
|
|
50
|
+
constructor(options) {
|
|
51
|
+
_classPrivateMethodInitSpec(this, _ExtensionManager_brand);
|
|
52
|
+
_classPrivateFieldInitSpec(this, _loader, void 0);
|
|
53
|
+
_classPrivateFieldInitSpec(this, _storage, void 0);
|
|
54
|
+
_classPrivateFieldInitSpec(this, _createHostBinding, void 0);
|
|
55
|
+
_classPrivateFieldInitSpec(this, _extensions, /* @__PURE__ */ new Map());
|
|
56
|
+
_classPrivateFieldInitSpec(this, _restored, false);
|
|
57
|
+
_classPrivateFieldSet2(_loader, this, options.loader);
|
|
58
|
+
_classPrivateFieldSet2(_storage, this, options.storage ?? null);
|
|
59
|
+
_classPrivateFieldSet2(_createHostBinding, this, options.createHostBinding ?? null);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load an extension from source code.
|
|
63
|
+
*
|
|
64
|
+
* The source is a JS object expression defining tools. Each tool has
|
|
65
|
+
* `description`, `parameters` (JSON Schema properties), optional
|
|
66
|
+
* `required` array, and an `execute` async function.
|
|
67
|
+
*
|
|
68
|
+
* @returns Summary of the loaded extension including discovered tools.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Restore extensions from DO storage after hibernation.
|
|
72
|
+
*
|
|
73
|
+
* Idempotent — skips extensions already in memory. Call this at the
|
|
74
|
+
* start of each chat turn (e.g. in onChatMessage before getTools).
|
|
75
|
+
*/
|
|
76
|
+
async restore() {
|
|
77
|
+
if (_classPrivateFieldGet2(_restored, this) || !_classPrivateFieldGet2(_storage, this)) return;
|
|
78
|
+
_classPrivateFieldSet2(_restored, this, true);
|
|
79
|
+
const entries = await _classPrivateFieldGet2(_storage, this).list({ prefix: STORAGE_PREFIX });
|
|
80
|
+
for (const persisted of entries.values()) {
|
|
81
|
+
if (_classPrivateFieldGet2(_extensions, this).has(persisted.manifest.name)) continue;
|
|
82
|
+
await _assertClassBrand(_ExtensionManager_brand, this, _loadInternal).call(this, persisted.manifest, persisted.source);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async load(manifest, source) {
|
|
86
|
+
if (_classPrivateFieldGet2(_extensions, this).has(manifest.name)) throw new Error(`Extension "${manifest.name}" is already loaded. Unload it first.`);
|
|
87
|
+
const info = await _assertClassBrand(_ExtensionManager_brand, this, _loadInternal).call(this, manifest, source);
|
|
88
|
+
if (_classPrivateFieldGet2(_storage, this)) await _classPrivateFieldGet2(_storage, this).put(`${STORAGE_PREFIX}${manifest.name}`, {
|
|
89
|
+
manifest,
|
|
90
|
+
source
|
|
91
|
+
});
|
|
92
|
+
return info;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Unload an extension, removing its tools from the agent.
|
|
96
|
+
*/
|
|
97
|
+
async unload(name) {
|
|
98
|
+
const removed = _classPrivateFieldGet2(_extensions, this).delete(name);
|
|
99
|
+
if (removed && _classPrivateFieldGet2(_storage, this)) await _classPrivateFieldGet2(_storage, this).delete(`${STORAGE_PREFIX}${name}`);
|
|
100
|
+
return removed;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* List all loaded extensions.
|
|
104
|
+
*/
|
|
105
|
+
list() {
|
|
106
|
+
return [..._classPrivateFieldGet2(_extensions, this).values()].map((ext) => toExtensionInfo(ext.manifest, ext.tools));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get AI SDK tools from all loaded extensions.
|
|
110
|
+
*
|
|
111
|
+
* Tool names are prefixed with the sanitized extension name to avoid
|
|
112
|
+
* collisions: e.g. extension "github" with tool "create_pr" → "github_create_pr".
|
|
113
|
+
*/
|
|
114
|
+
getTools() {
|
|
115
|
+
const tools = {};
|
|
116
|
+
for (const ext of _classPrivateFieldGet2(_extensions, this).values()) {
|
|
117
|
+
const prefix = sanitizeName(ext.manifest.name);
|
|
118
|
+
for (const descriptor of ext.tools) {
|
|
119
|
+
const toolName = `${prefix}_${descriptor.name}`;
|
|
120
|
+
tools[toolName] = tool({
|
|
121
|
+
description: `[${ext.manifest.name}] ${descriptor.description}`,
|
|
122
|
+
inputSchema: jsonSchema(descriptor.inputSchema),
|
|
123
|
+
execute: async (args) => {
|
|
124
|
+
if (!_classPrivateFieldGet2(_extensions, this).has(ext.manifest.name)) throw new Error(`Extension "${ext.manifest.name}" has been unloaded. Tool "${toolName}" is no longer available.`);
|
|
125
|
+
const resultJson = await ext.entrypoint.execute(descriptor.name, JSON.stringify(args));
|
|
126
|
+
const parsed = JSON.parse(resultJson);
|
|
127
|
+
if (parsed.error) throw new Error(parsed.error);
|
|
128
|
+
return parsed.result;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return tools;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
async function _loadInternal(manifest, source) {
|
|
137
|
+
const workerModule = wrapExtensionSource(source);
|
|
138
|
+
const permissions = manifest.permissions ?? {};
|
|
139
|
+
const workerEnv = {};
|
|
140
|
+
const wsLevel = permissions.workspace ?? "none";
|
|
141
|
+
if (_classPrivateFieldGet2(_createHostBinding, this) && wsLevel !== "none") workerEnv.host = _classPrivateFieldGet2(_createHostBinding, this).call(this, permissions);
|
|
142
|
+
const entrypoint = _classPrivateFieldGet2(_loader, this).get(`ext-${manifest.name}-${manifest.version}-${Date.now()}`, () => ({
|
|
143
|
+
compatibilityDate: "2025-06-01",
|
|
144
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
145
|
+
mainModule: "extension.js",
|
|
146
|
+
modules: { "extension.js": workerModule },
|
|
147
|
+
globalOutbound: permissions.network?.length ? void 0 : null,
|
|
148
|
+
...Object.keys(workerEnv).length > 0 ? { env: workerEnv } : {}
|
|
149
|
+
})).getEntrypoint();
|
|
150
|
+
const descriptorsJson = await entrypoint.describe();
|
|
151
|
+
const tools = JSON.parse(descriptorsJson);
|
|
152
|
+
_classPrivateFieldGet2(_extensions, this).set(manifest.name, {
|
|
153
|
+
manifest,
|
|
154
|
+
tools,
|
|
155
|
+
entrypoint
|
|
156
|
+
});
|
|
157
|
+
return toExtensionInfo(manifest, tools);
|
|
158
|
+
}
|
|
159
|
+
function toExtensionInfo(manifest, tools) {
|
|
160
|
+
const prefix = sanitizeName(manifest.name);
|
|
161
|
+
return {
|
|
162
|
+
name: manifest.name,
|
|
163
|
+
version: manifest.version,
|
|
164
|
+
description: manifest.description,
|
|
165
|
+
tools: tools.map((t) => `${prefix}_${t.name}`),
|
|
166
|
+
permissions: manifest.permissions ?? {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wrap an extension source (JS object expression) in a Worker module
|
|
171
|
+
* that exposes describe() and execute() RPC methods.
|
|
172
|
+
*/
|
|
173
|
+
function wrapExtensionSource(source) {
|
|
174
|
+
return `import { WorkerEntrypoint } from "cloudflare:workers";
|
|
175
|
+
|
|
176
|
+
const __tools = (${source});
|
|
177
|
+
|
|
178
|
+
export default class Extension extends WorkerEntrypoint {
|
|
179
|
+
describe() {
|
|
180
|
+
const descriptors = [];
|
|
181
|
+
for (const [name, def] of Object.entries(__tools)) {
|
|
182
|
+
descriptors.push({
|
|
183
|
+
name,
|
|
184
|
+
description: def.description || name,
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: def.parameters || {},
|
|
188
|
+
required: def.required || []
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return JSON.stringify(descriptors);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async execute(toolName, argsJson) {
|
|
196
|
+
const def = __tools[toolName];
|
|
197
|
+
if (!def || !def.execute) {
|
|
198
|
+
return JSON.stringify({ error: "Unknown tool: " + toolName });
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const args = JSON.parse(argsJson);
|
|
202
|
+
const result = await def.execute(args, this.env.host ?? null);
|
|
203
|
+
return JSON.stringify({ result });
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return JSON.stringify({ error: err.message || String(err) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { sanitizeName as n, ExtensionManager as t };
|
|
213
|
+
|
|
214
|
+
//# sourceMappingURL=manager-DIV0gQf3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-DIV0gQf3.js","names":[],"sources":["../src/extensions/manager.ts"],"sourcesContent":["/**\n * ExtensionManager — loads, manages, and exposes tools from extension Workers.\n *\n * Extensions are sandboxed Workers created via WorkerLoader. Each extension\n * declares tools (with JSON Schema inputs) and permissions. The manager:\n *\n * 1. Wraps extension source in a Worker module with describe/execute RPC\n * 2. Loads it via WorkerLoader with permission-gated bindings\n * 3. Discovers tools via describe() RPC call\n * 4. Exposes them as AI SDK tools via getTools()\n *\n * Extension source format — a JS object expression defining tools:\n *\n * ```js\n * ({\n * greet: {\n * description: \"Greet someone\",\n * parameters: { name: { type: \"string\" } },\n * required: [\"name\"],\n * execute: async (args, host) => `Hello, ${args.name}!`\n * }\n * })\n * ```\n *\n * The `host` parameter in execute is provided via `env.host` — a loopback\n * binding that resolves the parent agent and delegates workspace operations\n * (gated by permissions). See HostBridgeLoopback.\n */\n\nimport { tool, jsonSchema } from \"ai\";\nimport type { ToolSet } from \"ai\";\nimport type {\n ExtensionManifest,\n ExtensionPermissions,\n ExtensionInfo,\n ExtensionToolDescriptor\n} from \"./types\";\n\n/**\n * Sanitize a name for use as a tool name prefix.\n * Replaces any non-alphanumeric characters with underscores and\n * collapses consecutive underscores.\n */\nexport function sanitizeName(name: string): string {\n if (!name || name.trim().length === 0) {\n throw new Error(\"Extension name must not be empty\");\n }\n return name\n .replace(/[^a-zA-Z0-9]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\");\n}\n\ninterface ExtensionEntrypoint {\n describe(): Promise<string>;\n execute(toolName: string, argsJson: string): Promise<string>;\n}\n\ninterface LoadedExtension {\n manifest: ExtensionManifest;\n tools: ExtensionToolDescriptor[];\n entrypoint: ExtensionEntrypoint;\n}\n\n/** Shape persisted to DO storage for each extension. */\ninterface PersistedExtension {\n manifest: ExtensionManifest;\n source: string;\n}\n\nconst STORAGE_PREFIX = \"ext:\";\n\nexport interface ExtensionManagerOptions {\n /** WorkerLoader binding for creating sandboxed extension Workers. */\n loader: WorkerLoader;\n /**\n * Durable Object storage for persisting extensions across hibernation.\n * If provided, loaded extensions survive DO restarts. Call `restore()`\n * on each turn to rebuild in-memory state from storage.\n */\n storage?: DurableObjectStorage;\n /**\n * Factory that creates a loopback Fetcher for workspace access, given\n * an extension's declared permissions. The returned binding is injected\n * into the extension worker's `env.host`.\n *\n * If not provided, extensions receive no host binding (workspace tools\n * will get `null` for the host parameter).\n *\n * Typically wired up using HostBridgeLoopback via `ctx.exports`:\n * ```typescript\n * createHostBinding: (permissions) =>\n * ctx.exports.HostBridgeLoopback({\n * props: { agentClassName: \"ChatSession\", agentId: ctx.id.toString(), permissions }\n * })\n * ```\n */\n createHostBinding?: (permissions: ExtensionPermissions) => Fetcher;\n}\n\nexport class ExtensionManager {\n #loader: WorkerLoader;\n #storage: DurableObjectStorage | null;\n #createHostBinding: ((permissions: ExtensionPermissions) => Fetcher) | null;\n #extensions = new Map<string, LoadedExtension>();\n #restored = false;\n\n constructor(options: ExtensionManagerOptions) {\n this.#loader = options.loader;\n this.#storage = options.storage ?? null;\n this.#createHostBinding = options.createHostBinding ?? null;\n }\n\n /**\n * Load an extension from source code.\n *\n * The source is a JS object expression defining tools. Each tool has\n * `description`, `parameters` (JSON Schema properties), optional\n * `required` array, and an `execute` async function.\n *\n * @returns Summary of the loaded extension including discovered tools.\n */\n /**\n * Restore extensions from DO storage after hibernation.\n *\n * Idempotent — skips extensions already in memory. Call this at the\n * start of each chat turn (e.g. in onChatMessage before getTools).\n */\n async restore(): Promise<void> {\n if (this.#restored || !this.#storage) return;\n this.#restored = true;\n\n const entries = await this.#storage.list<PersistedExtension>({\n prefix: STORAGE_PREFIX\n });\n\n for (const persisted of entries.values()) {\n if (this.#extensions.has(persisted.manifest.name)) continue;\n await this.#loadInternal(persisted.manifest, persisted.source);\n }\n }\n\n async load(\n manifest: ExtensionManifest,\n source: string\n ): Promise<ExtensionInfo> {\n if (this.#extensions.has(manifest.name)) {\n throw new Error(\n `Extension \"${manifest.name}\" is already loaded. Unload it first.`\n );\n }\n\n const info = await this.#loadInternal(manifest, source);\n\n // Persist to storage so it survives hibernation\n if (this.#storage) {\n await this.#storage.put<PersistedExtension>(\n `${STORAGE_PREFIX}${manifest.name}`,\n { manifest, source }\n );\n }\n\n return info;\n }\n\n async #loadInternal(\n manifest: ExtensionManifest,\n source: string\n ): Promise<ExtensionInfo> {\n const workerModule = wrapExtensionSource(source);\n const permissions = manifest.permissions ?? {};\n\n // Build env bindings for the dynamic worker. If a host binding\n // factory is configured and the extension declares workspace\n // access, inject a loopback Fetcher as env.host.\n const workerEnv: Record<string, Fetcher> = {};\n const wsLevel = permissions.workspace ?? \"none\";\n if (this.#createHostBinding && wsLevel !== \"none\") {\n workerEnv.host = this.#createHostBinding(permissions);\n }\n\n const worker = this.#loader.get(\n `ext-${manifest.name}-${manifest.version}-${Date.now()}`,\n () => ({\n compatibilityDate: \"2025-06-01\",\n compatibilityFlags: [\"nodejs_compat\"],\n mainModule: \"extension.js\",\n modules: { \"extension.js\": workerModule },\n globalOutbound: permissions.network?.length ? undefined : null,\n ...(Object.keys(workerEnv).length > 0 ? { env: workerEnv } : {})\n })\n );\n\n const entrypoint = worker.getEntrypoint() as unknown as ExtensionEntrypoint;\n\n // Discover tools via RPC\n const descriptorsJson = await entrypoint.describe();\n const tools = JSON.parse(descriptorsJson) as ExtensionToolDescriptor[];\n\n this.#extensions.set(manifest.name, { manifest, tools, entrypoint });\n\n return toExtensionInfo(manifest, tools);\n }\n\n /**\n * Unload an extension, removing its tools from the agent.\n */\n async unload(name: string): Promise<boolean> {\n const removed = this.#extensions.delete(name);\n if (removed && this.#storage) {\n await this.#storage.delete(`${STORAGE_PREFIX}${name}`);\n }\n return removed;\n }\n\n /**\n * List all loaded extensions.\n */\n list(): ExtensionInfo[] {\n return [...this.#extensions.values()].map((ext) =>\n toExtensionInfo(ext.manifest, ext.tools)\n );\n }\n\n /**\n * Get AI SDK tools from all loaded extensions.\n *\n * Tool names are prefixed with the sanitized extension name to avoid\n * collisions: e.g. extension \"github\" with tool \"create_pr\" → \"github_create_pr\".\n */\n getTools(): ToolSet {\n const tools: ToolSet = {};\n\n for (const ext of this.#extensions.values()) {\n const prefix = sanitizeName(ext.manifest.name);\n\n for (const descriptor of ext.tools) {\n const toolName = `${prefix}_${descriptor.name}`;\n\n tools[toolName] = tool({\n description: `[${ext.manifest.name}] ${descriptor.description}`,\n inputSchema: jsonSchema(\n descriptor.inputSchema as Record<string, unknown>\n ),\n execute: async (args: Record<string, unknown>) => {\n if (!this.#extensions.has(ext.manifest.name)) {\n throw new Error(\n `Extension \"${ext.manifest.name}\" has been unloaded. Tool \"${toolName}\" is no longer available.`\n );\n }\n const resultJson = await ext.entrypoint.execute(\n descriptor.name,\n JSON.stringify(args)\n );\n const parsed = JSON.parse(resultJson) as {\n result?: unknown;\n error?: string;\n };\n if (parsed.error) throw new Error(parsed.error);\n return parsed.result;\n }\n });\n }\n }\n\n return tools;\n }\n}\n\nfunction toExtensionInfo(\n manifest: ExtensionManifest,\n tools: ExtensionToolDescriptor[]\n): ExtensionInfo {\n const prefix = sanitizeName(manifest.name);\n return {\n name: manifest.name,\n version: manifest.version,\n description: manifest.description,\n tools: tools.map((t) => `${prefix}_${t.name}`),\n permissions: manifest.permissions ?? {}\n };\n}\n\n/**\n * Wrap an extension source (JS object expression) in a Worker module\n * that exposes describe() and execute() RPC methods.\n */\nfunction wrapExtensionSource(source: string): string {\n return `import { WorkerEntrypoint } from \"cloudflare:workers\";\n\nconst __tools = (${source});\n\nexport default class Extension extends WorkerEntrypoint {\n describe() {\n const descriptors = [];\n for (const [name, def] of Object.entries(__tools)) {\n descriptors.push({\n name,\n description: def.description || name,\n inputSchema: {\n type: \"object\",\n properties: def.parameters || {},\n required: def.required || []\n }\n });\n }\n return JSON.stringify(descriptors);\n }\n\n async execute(toolName, argsJson) {\n const def = __tools[toolName];\n if (!def || !def.execute) {\n return JSON.stringify({ error: \"Unknown tool: \" + toolName });\n }\n try {\n const args = JSON.parse(argsJson);\n const result = await def.execute(args, this.env.host ?? null);\n return JSON.stringify({ result });\n } catch (err) {\n return JSON.stringify({ error: err.message || String(err) });\n }\n }\n}\n`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { UIMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
//#region src/message-builder.d.ts
|
|
4
|
+
/** The parts array type from UIMessage */
|
|
5
|
+
type MessageParts = UIMessage["parts"];
|
|
6
|
+
/** A single part from the UIMessage parts array */
|
|
7
|
+
type MessagePart = MessageParts[number];
|
|
8
|
+
/**
|
|
9
|
+
* Parsed chunk data from an AI SDK stream event.
|
|
10
|
+
* This is the JSON-parsed body of a CF_AGENT_USE_CHAT_RESPONSE message,
|
|
11
|
+
* or the `data:` payload of an SSE line.
|
|
12
|
+
*/
|
|
13
|
+
type StreamChunkData = {
|
|
14
|
+
type: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
delta?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
mediaType?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
sourceId?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
filename?: string;
|
|
23
|
+
toolCallId?: string;
|
|
24
|
+
toolName?: string;
|
|
25
|
+
input?: unknown;
|
|
26
|
+
inputTextDelta?: string;
|
|
27
|
+
output?: unknown;
|
|
28
|
+
state?: string;
|
|
29
|
+
errorText?: string;
|
|
30
|
+
preliminary?: boolean;
|
|
31
|
+
approvalId?: string;
|
|
32
|
+
providerMetadata?: Record<string, unknown>;
|
|
33
|
+
providerExecuted?: boolean;
|
|
34
|
+
data?: unknown;
|
|
35
|
+
transient?: boolean;
|
|
36
|
+
messageId?: string;
|
|
37
|
+
messageMetadata?: Record<string, unknown>;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Applies a stream chunk to a mutable parts array, building up the message
|
|
42
|
+
* incrementally. Returns true if the chunk was handled, false if it was
|
|
43
|
+
* an unrecognized type (caller may handle it with additional logic).
|
|
44
|
+
*/
|
|
45
|
+
declare function applyChunkToParts(
|
|
46
|
+
parts: MessagePart[],
|
|
47
|
+
chunk: StreamChunkData
|
|
48
|
+
): boolean;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { MessagePart, MessageParts, StreamChunkData, applyChunkToParts };
|
|
51
|
+
//# sourceMappingURL=message-builder.d.ts.map
|