@ixo/oracle-runtime-workers 0.13.0 → 0.16.0
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 +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +310 -0
- package/src/channels/contract.ts +246 -0
- package/src/channels/turns.ts +280 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +2003 -274
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +562 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pieces of the `ctx.kv` surface (`UserKvSurface`): argument checks,
|
|
3
|
+
* JSON encoding, the hard storage caps, and an in-memory implementation for
|
|
4
|
+
* tests and partial hosts. The Workers host backs the surface with the
|
|
5
|
+
* user's SQLite file instead (`sqlite/user-kv-store.ts`), with the same
|
|
6
|
+
* semantics.
|
|
7
|
+
*
|
|
8
|
+
* The caps exist because the rows share a file with the user's LangGraph
|
|
9
|
+
* checkpoints, sessions and transcript, and that file is exported as the
|
|
10
|
+
* owner copy and re-imported on a cold start: a plugin must not be able to
|
|
11
|
+
* grow it without bound, whatever options it passes.
|
|
12
|
+
*/
|
|
13
|
+
import type { UserKvSurface, UserKvWriteOptions } from '../plugin-api/types';
|
|
14
|
+
|
|
15
|
+
/** Longest namespace / key accepted, in characters. */
|
|
16
|
+
export const USER_KV_MAX_NAME_LENGTH = 512;
|
|
17
|
+
|
|
18
|
+
/** Largest stored value: UTF-8 bytes of its JSON text. */
|
|
19
|
+
export const USER_KV_MAX_VALUE_BYTES = 256 * 1024;
|
|
20
|
+
/** Most rows one namespace may hold (also the largest `maxEntries`). */
|
|
21
|
+
export const USER_KV_MAX_ENTRIES_PER_NAMESPACE = 10_000;
|
|
22
|
+
/** Most rows the user's store may hold across all namespaces. */
|
|
23
|
+
export const USER_KV_MAX_TOTAL_ENTRIES = 50_000;
|
|
24
|
+
/**
|
|
25
|
+
* Most bytes the user's store may hold across all namespaces, counting each
|
|
26
|
+
* row's namespace, key and value JSON ({@link userKvRowBytes}).
|
|
27
|
+
*/
|
|
28
|
+
export const USER_KV_MAX_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
/** The hard caps one store enforces. */
|
|
31
|
+
export interface UserKvLimits {
|
|
32
|
+
maxValueBytes: number;
|
|
33
|
+
maxEntriesPerNamespace: number;
|
|
34
|
+
maxTotalEntries: number;
|
|
35
|
+
maxTotalBytes: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const USER_KV_LIMITS: Readonly<UserKvLimits> = Object.freeze({
|
|
39
|
+
maxValueBytes: USER_KV_MAX_VALUE_BYTES,
|
|
40
|
+
maxEntriesPerNamespace: USER_KV_MAX_ENTRIES_PER_NAMESPACE,
|
|
41
|
+
maxTotalEntries: USER_KV_MAX_TOTAL_ENTRIES,
|
|
42
|
+
maxTotalBytes: USER_KV_MAX_TOTAL_BYTES,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The caps a store runs with: the defaults, with `overrides` (tests only;
|
|
47
|
+
* the host never passes any) replacing individual ones.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveUserKvLimits(
|
|
50
|
+
overrides: Partial<UserKvLimits> = {},
|
|
51
|
+
): Readonly<UserKvLimits> {
|
|
52
|
+
const limits = { ...USER_KV_LIMITS, ...overrides };
|
|
53
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
54
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
55
|
+
throw new RangeError(
|
|
56
|
+
`user kv limit ${name} must be a positive integer, got ${value}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze(limits);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Which cap a refused write would have broken. */
|
|
64
|
+
export type UserKvLimitKind =
|
|
65
|
+
| 'value'
|
|
66
|
+
| 'namespace-entries'
|
|
67
|
+
| 'entries'
|
|
68
|
+
| 'bytes';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A write `ctx.kv` refused because it would break a hard cap. Nothing was
|
|
72
|
+
* written and nothing was evicted: the previous value, if any, is still in
|
|
73
|
+
* place. `actual` is what the store (or, for `value`, the value) would have
|
|
74
|
+
* measured after the write; `max` is the cap.
|
|
75
|
+
*/
|
|
76
|
+
export class UserKvLimitError extends Error {
|
|
77
|
+
override readonly name = 'UserKvLimitError';
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
readonly limit: UserKvLimitKind,
|
|
81
|
+
readonly namespace: string,
|
|
82
|
+
readonly max: number,
|
|
83
|
+
readonly actual: number,
|
|
84
|
+
) {
|
|
85
|
+
super(userKvLimitMessage(limit, namespace, max, actual));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function userKvLimitMessage(
|
|
90
|
+
limit: UserKvLimitKind,
|
|
91
|
+
namespace: string,
|
|
92
|
+
max: number,
|
|
93
|
+
actual: number,
|
|
94
|
+
): string {
|
|
95
|
+
const where = `user kv write to namespace "${namespace}" refused`;
|
|
96
|
+
switch (limit) {
|
|
97
|
+
case 'value':
|
|
98
|
+
return `${where}: the value is ${actual} bytes of JSON, the cap is ${max}`;
|
|
99
|
+
case 'namespace-entries':
|
|
100
|
+
return `${where}: the namespace would hold ${actual} entries, the cap is ${max}`;
|
|
101
|
+
case 'entries':
|
|
102
|
+
return `${where}: the store would hold ${actual} entries across all namespaces, the cap is ${max}`;
|
|
103
|
+
case 'bytes':
|
|
104
|
+
return `${where}: the store would hold ${actual} bytes of namespace, key and value across all namespaces, the cap is ${max}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const utf8 = new TextEncoder();
|
|
109
|
+
|
|
110
|
+
/** UTF-8 byte length of a stored value's JSON text — what the value cap counts. */
|
|
111
|
+
export function userKvValueBytes(json: string): number {
|
|
112
|
+
return utf8.encode(json).byteLength;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* What one row adds to the store's byte total: UTF-8 bytes of its namespace,
|
|
117
|
+
* key and value JSON. Names count because each may be up to
|
|
118
|
+
* {@link USER_KV_MAX_NAME_LENGTH} characters, enough to carry far more than
|
|
119
|
+
* the byte cap across the entry cap if only values were counted.
|
|
120
|
+
*/
|
|
121
|
+
export function userKvRowBytes(
|
|
122
|
+
namespace: string,
|
|
123
|
+
key: string,
|
|
124
|
+
valueBytes: number,
|
|
125
|
+
): number {
|
|
126
|
+
return (
|
|
127
|
+
utf8.encode(namespace).byteLength + utf8.encode(key).byteLength + valueBytes
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The value's byte length; throws {@link UserKvLimitError} above the cap. */
|
|
132
|
+
export function assertUserKvValueSize(
|
|
133
|
+
namespace: string,
|
|
134
|
+
json: string,
|
|
135
|
+
limits: Readonly<UserKvLimits>,
|
|
136
|
+
): number {
|
|
137
|
+
const bytes = userKvValueBytes(json);
|
|
138
|
+
if (bytes > limits.maxValueBytes) {
|
|
139
|
+
throw new UserKvLimitError('value', namespace, limits.maxValueBytes, bytes);
|
|
140
|
+
}
|
|
141
|
+
return bytes;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** What a store holds after a write, as far as the caps are concerned. */
|
|
145
|
+
export interface UserKvUsage {
|
|
146
|
+
namespaceEntries: number;
|
|
147
|
+
totalEntries: number;
|
|
148
|
+
totalBytes: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The first cap a write breaks, or undefined. Only a measure the write grew
|
|
153
|
+
* is checked — a write that added a key for the entry counts, one that grew
|
|
154
|
+
* the stored bytes for the byte total — so a store already above a cap
|
|
155
|
+
* (a file written before the caps existed) can still shrink, replace in
|
|
156
|
+
* place and delete.
|
|
157
|
+
*/
|
|
158
|
+
export function userKvOverflow(
|
|
159
|
+
namespace: string,
|
|
160
|
+
usage: UserKvUsage,
|
|
161
|
+
grew: { entries: boolean; bytes: boolean },
|
|
162
|
+
limits: Readonly<UserKvLimits>,
|
|
163
|
+
): UserKvLimitError | undefined {
|
|
164
|
+
if (grew.entries) {
|
|
165
|
+
if (usage.namespaceEntries > limits.maxEntriesPerNamespace) {
|
|
166
|
+
return new UserKvLimitError(
|
|
167
|
+
'namespace-entries',
|
|
168
|
+
namespace,
|
|
169
|
+
limits.maxEntriesPerNamespace,
|
|
170
|
+
usage.namespaceEntries,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (usage.totalEntries > limits.maxTotalEntries) {
|
|
174
|
+
return new UserKvLimitError(
|
|
175
|
+
'entries',
|
|
176
|
+
namespace,
|
|
177
|
+
limits.maxTotalEntries,
|
|
178
|
+
usage.totalEntries,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (grew.bytes && usage.totalBytes > limits.maxTotalBytes) {
|
|
183
|
+
return new UserKvLimitError(
|
|
184
|
+
'bytes',
|
|
185
|
+
namespace,
|
|
186
|
+
limits.maxTotalBytes,
|
|
187
|
+
usage.totalBytes,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function assertUserKvName(
|
|
194
|
+
kind: 'namespace' | 'key',
|
|
195
|
+
value: string,
|
|
196
|
+
): void {
|
|
197
|
+
if (value.length === 0 || value.length > USER_KV_MAX_NAME_LENGTH) {
|
|
198
|
+
throw new RangeError(
|
|
199
|
+
`user kv ${kind} must be 1-${USER_KV_MAX_NAME_LENGTH} characters, got ${value.length}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate a write's options. A `maxEntries` above the per-namespace cap is
|
|
206
|
+
* refused rather than clamped, so a plugin learns its bound is not the one
|
|
207
|
+
* in force.
|
|
208
|
+
*/
|
|
209
|
+
export function assertUserKvOptions(
|
|
210
|
+
options: UserKvWriteOptions,
|
|
211
|
+
limits: Readonly<UserKvLimits> = USER_KV_LIMITS,
|
|
212
|
+
): void {
|
|
213
|
+
const { idleTtlMs, maxEntries } = options;
|
|
214
|
+
if (
|
|
215
|
+
idleTtlMs !== undefined &&
|
|
216
|
+
(!Number.isFinite(idleTtlMs) || idleTtlMs <= 0)
|
|
217
|
+
) {
|
|
218
|
+
throw new RangeError(
|
|
219
|
+
`user kv idleTtlMs must be a positive number, got ${idleTtlMs}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
maxEntries !== undefined &&
|
|
224
|
+
(!Number.isInteger(maxEntries) || maxEntries < 1)
|
|
225
|
+
) {
|
|
226
|
+
throw new RangeError(
|
|
227
|
+
`user kv maxEntries must be a positive integer, got ${maxEntries}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (maxEntries !== undefined && maxEntries > limits.maxEntriesPerNamespace) {
|
|
231
|
+
throw new RangeError(
|
|
232
|
+
`user kv maxEntries must be at most ${limits.maxEntriesPerNamespace} (the per-namespace cap), got ${maxEntries}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Serialise a value for storage. Rejects what JSON cannot carry (a bare
|
|
239
|
+
* `undefined`, a function, a symbol) instead of storing something the next
|
|
240
|
+
* read cannot return.
|
|
241
|
+
*/
|
|
242
|
+
export function encodeUserKvValue(value: unknown): string {
|
|
243
|
+
const json = JSON.stringify(value);
|
|
244
|
+
if (typeof json !== 'string') {
|
|
245
|
+
throw new TypeError('user kv values must be JSON-serialisable');
|
|
246
|
+
}
|
|
247
|
+
return json;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function decodeUserKvValue(json: string): unknown {
|
|
251
|
+
return JSON.parse(json);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface MemoryEntry {
|
|
255
|
+
json: string;
|
|
256
|
+
/** Row bytes ({@link userKvRowBytes}): namespace + key + value JSON. */
|
|
257
|
+
bytes: number;
|
|
258
|
+
idleTtlMs?: number;
|
|
259
|
+
expiresAt?: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* In-memory {@link UserKvSurface}: per-namespace maps whose insertion order
|
|
264
|
+
* is recency (a hit re-inserts the entry). Expiry is lazy, enforced on access
|
|
265
|
+
* and on bounded writes. Values are stored serialised, so callers get copies.
|
|
266
|
+
* Enforces the same hard caps as the SQLite store; `limits` overrides them
|
|
267
|
+
* for tests.
|
|
268
|
+
*/
|
|
269
|
+
export function createMemoryUserKv(
|
|
270
|
+
options: { now?: () => number; limits?: Partial<UserKvLimits> } = {},
|
|
271
|
+
): UserKvSurface {
|
|
272
|
+
const now = options.now ?? Date.now;
|
|
273
|
+
const limits = resolveUserKvLimits(options.limits);
|
|
274
|
+
const namespaces = new Map<string, Map<string, MemoryEntry>>();
|
|
275
|
+
|
|
276
|
+
const bucket = (namespace: string): Map<string, MemoryEntry> => {
|
|
277
|
+
let entries = namespaces.get(namespace);
|
|
278
|
+
if (!entries) {
|
|
279
|
+
entries = new Map();
|
|
280
|
+
namespaces.set(namespace, entries);
|
|
281
|
+
}
|
|
282
|
+
return entries;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const expired = (entry: MemoryEntry, cutoff: number): boolean =>
|
|
286
|
+
entry.expiresAt !== undefined && entry.expiresAt <= cutoff;
|
|
287
|
+
|
|
288
|
+
/** The live entry (refreshed and marked most recently used), or undefined. */
|
|
289
|
+
const touch = (
|
|
290
|
+
entries: Map<string, MemoryEntry>,
|
|
291
|
+
key: string,
|
|
292
|
+
): MemoryEntry | undefined => {
|
|
293
|
+
const entry = entries.get(key);
|
|
294
|
+
if (!entry) return undefined;
|
|
295
|
+
entries.delete(key);
|
|
296
|
+
if (expired(entry, now())) return undefined;
|
|
297
|
+
if (entry.idleTtlMs !== undefined) {
|
|
298
|
+
entry.expiresAt = now() + entry.idleTtlMs;
|
|
299
|
+
}
|
|
300
|
+
entries.set(key, entry);
|
|
301
|
+
return entry;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const usage = (entries: Map<string, MemoryEntry>): UserKvUsage => {
|
|
305
|
+
let totalEntries = 0;
|
|
306
|
+
let totalBytes = 0;
|
|
307
|
+
for (const ns of namespaces.values()) {
|
|
308
|
+
totalEntries += ns.size;
|
|
309
|
+
for (const entry of ns.values()) totalBytes += entry.bytes;
|
|
310
|
+
}
|
|
311
|
+
return { namespaceEntries: entries.size, totalEntries, totalBytes };
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** Drop expired entries of every namespace; returns how many went. */
|
|
315
|
+
const sweepExpired = (): number => {
|
|
316
|
+
const cutoff = now();
|
|
317
|
+
let swept = 0;
|
|
318
|
+
for (const ns of namespaces.values()) {
|
|
319
|
+
for (const [key, entry] of ns) {
|
|
320
|
+
if (expired(entry, cutoff)) {
|
|
321
|
+
ns.delete(key);
|
|
322
|
+
swept++;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return swept;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run `body` against `entries` all-or-nothing: if it throws, the namespace
|
|
331
|
+
* is put back exactly as it was (order, values, deadlines), the way the
|
|
332
|
+
* SQLite store's transaction rolls back.
|
|
333
|
+
*/
|
|
334
|
+
const atomically = <T>(
|
|
335
|
+
entries: Map<string, MemoryEntry>,
|
|
336
|
+
body: () => T,
|
|
337
|
+
): T => {
|
|
338
|
+
const before = [...entries].map(([key, entry]): [string, MemoryEntry] => [
|
|
339
|
+
key,
|
|
340
|
+
{ ...entry },
|
|
341
|
+
]);
|
|
342
|
+
try {
|
|
343
|
+
return body();
|
|
344
|
+
} catch (error) {
|
|
345
|
+
entries.clear();
|
|
346
|
+
for (const [key, entry] of before) entries.set(key, entry);
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Replace (or, for `json === undefined`, delete) the entry, apply the
|
|
353
|
+
* write's own `maxEntries` trim, then refuse with {@link UserKvLimitError}
|
|
354
|
+
* if a hard cap is broken even after the store-wide expired entries are
|
|
355
|
+
* swept. Callers run it inside {@link atomically}.
|
|
356
|
+
*/
|
|
357
|
+
const write = (
|
|
358
|
+
namespace: string,
|
|
359
|
+
entries: Map<string, MemoryEntry>,
|
|
360
|
+
key: string,
|
|
361
|
+
json: string | undefined,
|
|
362
|
+
opts: UserKvWriteOptions,
|
|
363
|
+
): void => {
|
|
364
|
+
const previous = entries.get(key);
|
|
365
|
+
const live =
|
|
366
|
+
previous !== undefined && !expired(previous, now())
|
|
367
|
+
? previous
|
|
368
|
+
: undefined;
|
|
369
|
+
entries.delete(key);
|
|
370
|
+
let bytes = 0;
|
|
371
|
+
if (json !== undefined) {
|
|
372
|
+
bytes = userKvRowBytes(
|
|
373
|
+
namespace,
|
|
374
|
+
key,
|
|
375
|
+
assertUserKvValueSize(namespace, json, limits),
|
|
376
|
+
);
|
|
377
|
+
const entry: MemoryEntry = { json, bytes };
|
|
378
|
+
if (opts.idleTtlMs !== undefined) {
|
|
379
|
+
entry.idleTtlMs = opts.idleTtlMs;
|
|
380
|
+
entry.expiresAt = now() + opts.idleTtlMs;
|
|
381
|
+
}
|
|
382
|
+
entries.set(key, entry);
|
|
383
|
+
}
|
|
384
|
+
if (opts.maxEntries !== undefined) {
|
|
385
|
+
const cutoff = now();
|
|
386
|
+
for (const [k, entry] of entries) {
|
|
387
|
+
if (expired(entry, cutoff)) entries.delete(k);
|
|
388
|
+
}
|
|
389
|
+
while (entries.size > opts.maxEntries) {
|
|
390
|
+
const oldest = entries.keys().next();
|
|
391
|
+
if (oldest.done) break;
|
|
392
|
+
entries.delete(oldest.value);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (json === undefined) return;
|
|
396
|
+
// The replaced row has the same namespace and key, so comparing row
|
|
397
|
+
// bytes compares value bytes.
|
|
398
|
+
const grew = {
|
|
399
|
+
entries: live === undefined,
|
|
400
|
+
bytes: bytes > (live?.bytes ?? 0),
|
|
401
|
+
};
|
|
402
|
+
if (!grew.entries && !grew.bytes) return;
|
|
403
|
+
let overflow = userKvOverflow(namespace, usage(entries), grew, limits);
|
|
404
|
+
if (overflow && sweepExpired() > 0) {
|
|
405
|
+
overflow = userKvOverflow(namespace, usage(entries), grew, limits);
|
|
406
|
+
}
|
|
407
|
+
if (overflow) throw overflow;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
async get(namespace, key) {
|
|
412
|
+
assertUserKvName('namespace', namespace);
|
|
413
|
+
assertUserKvName('key', key);
|
|
414
|
+
const entry = touch(bucket(namespace), key);
|
|
415
|
+
return entry ? decodeUserKvValue(entry.json) : undefined;
|
|
416
|
+
},
|
|
417
|
+
async set(namespace, key, value, opts = {}) {
|
|
418
|
+
assertUserKvName('namespace', namespace);
|
|
419
|
+
assertUserKvName('key', key);
|
|
420
|
+
assertUserKvOptions(opts, limits);
|
|
421
|
+
const json = encodeUserKvValue(value);
|
|
422
|
+
const entries = bucket(namespace);
|
|
423
|
+
atomically(entries, () => write(namespace, entries, key, json, opts));
|
|
424
|
+
},
|
|
425
|
+
async update(namespace, key, fn, opts = {}) {
|
|
426
|
+
assertUserKvName('namespace', namespace);
|
|
427
|
+
assertUserKvName('key', key);
|
|
428
|
+
assertUserKvOptions(opts, limits);
|
|
429
|
+
const entries = bucket(namespace);
|
|
430
|
+
const json = atomically(entries, () => {
|
|
431
|
+
const entry = touch(entries, key);
|
|
432
|
+
const next = fn(entry ? decodeUserKvValue(entry.json) : undefined);
|
|
433
|
+
const encoded =
|
|
434
|
+
next === undefined ? undefined : encodeUserKvValue(next);
|
|
435
|
+
write(namespace, entries, key, encoded, opts);
|
|
436
|
+
return encoded;
|
|
437
|
+
});
|
|
438
|
+
return json === undefined ? undefined : decodeUserKvValue(json);
|
|
439
|
+
},
|
|
440
|
+
async delete(namespace, key) {
|
|
441
|
+
assertUserKvName('namespace', namespace);
|
|
442
|
+
assertUserKvName('key', key);
|
|
443
|
+
bucket(namespace).delete(key);
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Command } from '@langchain/langgraph';
|
|
|
2
2
|
import { capToolResult, type ResultCapConfig } from './middlewares/result-cap';
|
|
3
3
|
import { tool } from '@langchain/core/tools';
|
|
4
4
|
import type { StructuredTool } from 'langchain';
|
|
5
|
+
import { requireToolPlane, toolPlaneOf } from '../plugin-api/tool-plane';
|
|
5
6
|
import type {
|
|
6
7
|
PluginTool,
|
|
7
8
|
RuntimeContext,
|
|
@@ -23,6 +24,12 @@ export interface WrapPluginToolOptions {
|
|
|
23
24
|
ambient: AmbientServices;
|
|
24
25
|
/** Snapshot of the graph state for the in-flight build. */
|
|
25
26
|
state: RuntimeStateInput;
|
|
27
|
+
/**
|
|
28
|
+
* The contributing plugin. It names an admin tool's capability
|
|
29
|
+
* (`adminToolResource`), so an admin tool cannot be wrapped without it;
|
|
30
|
+
* runtime tools (the meta-tools) omit it.
|
|
31
|
+
*/
|
|
32
|
+
pluginName?: string;
|
|
26
33
|
/** Plugin manifest title used to auto-prefix the description. */
|
|
27
34
|
pluginTitle?: string;
|
|
28
35
|
/**
|
|
@@ -76,6 +83,11 @@ export function resolveRunConfig(
|
|
|
76
83
|
*
|
|
77
84
|
* The agent-facing description is auto-prefixed with the plugin's
|
|
78
85
|
* `manifest.title` so the agent always knows which plugin a tool belongs to.
|
|
86
|
+
*
|
|
87
|
+
* An admin-plane tool is checked against the call's delegation right before
|
|
88
|
+
* its handler runs (`requireToolPlane`), whatever discovery and binding
|
|
89
|
+
* decided: a host that binds it without the turn's filtering still cannot
|
|
90
|
+
* run it for a user whose delegation does not grant it.
|
|
79
91
|
*/
|
|
80
92
|
export function wrapPluginTool(
|
|
81
93
|
pluginTool: PluginTool,
|
|
@@ -84,11 +96,17 @@ export function wrapPluginTool(
|
|
|
84
96
|
const {
|
|
85
97
|
ambient,
|
|
86
98
|
state,
|
|
99
|
+
pluginName,
|
|
87
100
|
pluginTitle,
|
|
88
101
|
sharedFactory,
|
|
89
102
|
fallbackContext,
|
|
90
103
|
resultCap,
|
|
91
104
|
} = options;
|
|
105
|
+
if (toolPlaneOf(pluginTool) === 'admin' && pluginName === undefined) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`wrapPluginTool: admin tool "${pluginTool.name}" needs \`pluginName\` — its capability is named by plugin and tool.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
92
110
|
const description = pluginTitle
|
|
93
111
|
? `[${pluginTitle}] ${pluginTool.description}`
|
|
94
112
|
: pluginTool.description;
|
|
@@ -104,6 +122,8 @@ export function wrapPluginTool(
|
|
|
104
122
|
state,
|
|
105
123
|
sharedFactory,
|
|
106
124
|
);
|
|
125
|
+
if (pluginName !== undefined)
|
|
126
|
+
requireToolPlane(ctx, pluginName, pluginTool);
|
|
107
127
|
const output = await pluginTool.handler(args, ctx);
|
|
108
128
|
if (!resultCap || output instanceof Command || output == null)
|
|
109
129
|
return output;
|