@ixo/oracle-runtime-workers 0.2.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 +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal Mustache renderer covering exactly what the system-prompt
|
|
3
|
+
* template uses — replaces the `mustache` package the Node runtime pulled in
|
|
4
|
+
* through `@langchain/core`'s `PromptTemplate` (`templateFormat: 'mustache'`).
|
|
5
|
+
*
|
|
6
|
+
* Supported syntax (Mustache spec semantics for each):
|
|
7
|
+
* - `{{name}}` and `{{{name}}}` — interpolation. Both are unescaped, which
|
|
8
|
+
* is also how LangChain configures Mustache (`mustache.escape = identity`).
|
|
9
|
+
* - `{{#name}} … {{/name}}` — section, rendered once when the value is
|
|
10
|
+
* truthy (a non-empty string, `true`, a non-zero number, a non-empty
|
|
11
|
+
* array, or an object). Not iterated: the prompt has no list slots.
|
|
12
|
+
* - `{{^name}} … {{/name}}` — inverted section.
|
|
13
|
+
* - `{{! comment }}` — dropped.
|
|
14
|
+
* - Standalone tag lines: a section/inverted/close/comment tag that is the
|
|
15
|
+
* only non-whitespace on its line is removed together with that line's
|
|
16
|
+
* newline, exactly as the spec requires. Interpolation tags are never
|
|
17
|
+
* standalone.
|
|
18
|
+
*
|
|
19
|
+
* Unsupported on purpose: partials, dotted names, set-delimiters, and
|
|
20
|
+
* iteration. Values are looked up on the flat `values` record only.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type TemplateValues = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
type Node =
|
|
26
|
+
| { kind: 'text'; text: string }
|
|
27
|
+
| { kind: 'var'; name: string }
|
|
28
|
+
| { kind: 'section'; name: string; inverted: boolean; children: Node[] };
|
|
29
|
+
|
|
30
|
+
interface Token {
|
|
31
|
+
kind: 'text' | 'var' | 'open' | 'open-inverted' | 'close' | 'comment';
|
|
32
|
+
value: string;
|
|
33
|
+
/** Standalone-line detection needs to know where the tag sat in the source. */
|
|
34
|
+
start: number;
|
|
35
|
+
end: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TAG_RE =
|
|
39
|
+
/\{\{\{\s*([^{}]+?)\s*\}\}\}|\{\{\s*([#^/!])?\s*([^{}]*?)\s*\}\}/g;
|
|
40
|
+
|
|
41
|
+
function tokenize(template: string): Token[] {
|
|
42
|
+
const tokens: Token[] = [];
|
|
43
|
+
let last = 0;
|
|
44
|
+
for (const match of template.matchAll(TAG_RE)) {
|
|
45
|
+
const start = match.index ?? 0;
|
|
46
|
+
const end = start + match[0].length;
|
|
47
|
+
if (start > last) {
|
|
48
|
+
tokens.push({
|
|
49
|
+
kind: 'text',
|
|
50
|
+
value: template.slice(last, start),
|
|
51
|
+
start: last,
|
|
52
|
+
end: start,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const triple = match[1];
|
|
56
|
+
if (triple !== undefined) {
|
|
57
|
+
tokens.push({ kind: 'var', value: triple, start, end });
|
|
58
|
+
} else {
|
|
59
|
+
const sigil = match[2];
|
|
60
|
+
const name = match[3] ?? '';
|
|
61
|
+
const kind: Token['kind'] =
|
|
62
|
+
sigil === '#'
|
|
63
|
+
? 'open'
|
|
64
|
+
: sigil === '^'
|
|
65
|
+
? 'open-inverted'
|
|
66
|
+
: sigil === '/'
|
|
67
|
+
? 'close'
|
|
68
|
+
: sigil === '!'
|
|
69
|
+
? 'comment'
|
|
70
|
+
: 'var';
|
|
71
|
+
tokens.push({ kind, value: name, start, end });
|
|
72
|
+
}
|
|
73
|
+
last = end;
|
|
74
|
+
}
|
|
75
|
+
if (last < template.length) {
|
|
76
|
+
tokens.push({
|
|
77
|
+
kind: 'text',
|
|
78
|
+
value: template.slice(last),
|
|
79
|
+
start: last,
|
|
80
|
+
end: template.length,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return stripStandaloneLines(tokens);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove the surrounding whitespace + newline of every tag that stands alone
|
|
88
|
+
* on its line. Operates on the adjacent text tokens so the parse tree never
|
|
89
|
+
* sees the dead whitespace.
|
|
90
|
+
*/
|
|
91
|
+
function stripStandaloneLines(tokens: Token[]): Token[] {
|
|
92
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
93
|
+
const tok = tokens[i]!;
|
|
94
|
+
if (
|
|
95
|
+
tok.kind !== 'open' &&
|
|
96
|
+
tok.kind !== 'open-inverted' &&
|
|
97
|
+
tok.kind !== 'close' &&
|
|
98
|
+
tok.kind !== 'comment'
|
|
99
|
+
) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const prev = tokens[i - 1];
|
|
103
|
+
const next = tokens[i + 1];
|
|
104
|
+
|
|
105
|
+
// Text before the tag on the same line must be whitespace-only (or the
|
|
106
|
+
// tag must start the template / follow another tag's stripped line).
|
|
107
|
+
let prevOk: boolean;
|
|
108
|
+
let prevCut = 0;
|
|
109
|
+
if (prev === undefined) {
|
|
110
|
+
prevOk = true;
|
|
111
|
+
} else if (prev.kind !== 'text') {
|
|
112
|
+
prevOk = false;
|
|
113
|
+
} else {
|
|
114
|
+
const nl = prev.value.lastIndexOf('\n');
|
|
115
|
+
const tail = prev.value.slice(nl + 1);
|
|
116
|
+
prevOk = /^[ \t]*$/.test(tail);
|
|
117
|
+
prevCut = tail.length;
|
|
118
|
+
}
|
|
119
|
+
if (!prevOk) continue;
|
|
120
|
+
|
|
121
|
+
// Text after the tag up to and including the newline must be whitespace.
|
|
122
|
+
let nextOk: boolean;
|
|
123
|
+
let nextCut = 0;
|
|
124
|
+
if (next === undefined) {
|
|
125
|
+
nextOk = true;
|
|
126
|
+
} else if (next.kind !== 'text') {
|
|
127
|
+
nextOk = false;
|
|
128
|
+
} else {
|
|
129
|
+
const m = /^[ \t]*(\r?\n|$)/.exec(next.value);
|
|
130
|
+
nextOk = m !== null;
|
|
131
|
+
nextCut = m ? m[0].length : 0;
|
|
132
|
+
}
|
|
133
|
+
if (!nextOk) continue;
|
|
134
|
+
|
|
135
|
+
if (prev && prev.kind === 'text' && prevCut > 0) {
|
|
136
|
+
prev.value = prev.value.slice(0, prev.value.length - prevCut);
|
|
137
|
+
}
|
|
138
|
+
if (next && next.kind === 'text' && nextCut > 0) {
|
|
139
|
+
next.value = next.value.slice(nextCut);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return tokens.filter((t) => !(t.kind === 'text' && t.value.length === 0));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parse(tokens: Token[]): Node[] {
|
|
146
|
+
const root: Node[] = [];
|
|
147
|
+
const stack: Array<{ name: string; children: Node[] }> = [];
|
|
148
|
+
let current = root;
|
|
149
|
+
|
|
150
|
+
for (const tok of tokens) {
|
|
151
|
+
switch (tok.kind) {
|
|
152
|
+
case 'text':
|
|
153
|
+
current.push({ kind: 'text', text: tok.value });
|
|
154
|
+
break;
|
|
155
|
+
case 'var':
|
|
156
|
+
current.push({ kind: 'var', name: tok.value });
|
|
157
|
+
break;
|
|
158
|
+
case 'comment':
|
|
159
|
+
break;
|
|
160
|
+
case 'open':
|
|
161
|
+
case 'open-inverted': {
|
|
162
|
+
const node: Node = {
|
|
163
|
+
kind: 'section',
|
|
164
|
+
name: tok.value,
|
|
165
|
+
inverted: tok.kind === 'open-inverted',
|
|
166
|
+
children: [],
|
|
167
|
+
};
|
|
168
|
+
current.push(node);
|
|
169
|
+
stack.push({ name: tok.value, children: current });
|
|
170
|
+
current = node.children;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'close': {
|
|
174
|
+
const open = stack.pop();
|
|
175
|
+
if (!open || open.name !== tok.value) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`template: unbalanced section — closing "${tok.value}" ${
|
|
178
|
+
open ? `while "${open.name}" is open` : 'with no open section'
|
|
179
|
+
}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
current = open.children;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (stack.length > 0) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`template: section "${stack[stack.length - 1]!.name}" was never closed`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return root;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isTruthy(value: unknown): boolean {
|
|
196
|
+
if (value === null || value === undefined || value === false) return false;
|
|
197
|
+
if (typeof value === 'string') return value.length > 0;
|
|
198
|
+
if (typeof value === 'number') return value !== 0;
|
|
199
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function stringify(value: unknown): string {
|
|
204
|
+
if (value === null || value === undefined) return '';
|
|
205
|
+
return typeof value === 'string' ? value : String(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function render(nodes: Node[], values: TemplateValues): string {
|
|
209
|
+
let out = '';
|
|
210
|
+
for (const node of nodes) {
|
|
211
|
+
switch (node.kind) {
|
|
212
|
+
case 'text':
|
|
213
|
+
out += node.text;
|
|
214
|
+
break;
|
|
215
|
+
case 'var':
|
|
216
|
+
out += stringify(values[node.name]);
|
|
217
|
+
break;
|
|
218
|
+
case 'section': {
|
|
219
|
+
const truthy = isTruthy(values[node.name]);
|
|
220
|
+
if (truthy !== node.inverted) out += render(node.children, values);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A parsed template that can be rendered repeatedly. */
|
|
229
|
+
export interface CompiledTemplate {
|
|
230
|
+
render(values: TemplateValues): string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Parse once, render many. Throws on unbalanced sections. */
|
|
234
|
+
export function compileTemplate(template: string): CompiledTemplate {
|
|
235
|
+
const nodes = parse(tokenize(template));
|
|
236
|
+
return { render: (values) => render(nodes, values) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** One-shot convenience for `compileTemplate(template).render(values)`. */
|
|
240
|
+
export function renderTemplate(
|
|
241
|
+
template: string,
|
|
242
|
+
values: TemplateValues,
|
|
243
|
+
): string {
|
|
244
|
+
return compileTemplate(template).render(values);
|
|
245
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Logger } from '../plugin-api/types';
|
|
3
|
+
|
|
4
|
+
// ── Logger defaults ─────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/** A logger that discards everything. The default wherever none is supplied. */
|
|
7
|
+
export const NOOP_LOGGER: Logger = Object.freeze({
|
|
8
|
+
log: () => undefined,
|
|
9
|
+
warn: () => undefined,
|
|
10
|
+
error: () => undefined,
|
|
11
|
+
debug: () => undefined,
|
|
12
|
+
verbose: () => undefined,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A `console`-backed logger whose `child()` prefixes every record with the
|
|
17
|
+
* bound context (`[plugin=weather]`). Suitable for Workers, where `console`
|
|
18
|
+
* output is what `wrangler tail` and the dashboard show.
|
|
19
|
+
*/
|
|
20
|
+
export function createConsoleLogger(
|
|
21
|
+
bindings: Record<string, unknown> = {},
|
|
22
|
+
): Logger {
|
|
23
|
+
const prefix = Object.entries(bindings)
|
|
24
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
25
|
+
.join(' ');
|
|
26
|
+
const tag = prefix.length > 0 ? `[${prefix}]` : '';
|
|
27
|
+
const emit =
|
|
28
|
+
(fn: (...args: unknown[]) => void) =>
|
|
29
|
+
(message: unknown, ...optional: unknown[]): void => {
|
|
30
|
+
if (tag) fn(tag, message, ...optional);
|
|
31
|
+
else fn(message, ...optional);
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
log: emit(console.log),
|
|
35
|
+
warn: emit(console.warn),
|
|
36
|
+
error: emit(console.error),
|
|
37
|
+
debug: emit(console.debug),
|
|
38
|
+
verbose: emit(console.debug),
|
|
39
|
+
child: (more) => createConsoleLogger({ ...bindings, ...more }),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Tool lock ───────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Locks live per isolate. Keys are `${sessionId}:${toolName}` by convention,
|
|
47
|
+
* so users sharing an isolate never contend — a lock only ever guards the
|
|
48
|
+
* same session re-entering the same stateful tool.
|
|
49
|
+
*/
|
|
50
|
+
const activeLocks = new Set<string>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Acquire an exclusive lock for the given key. Throws immediately if the key
|
|
54
|
+
* is already locked (another invocation is in flight). Returns a release
|
|
55
|
+
* function — always call it in a `finally` block.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const release = acquireToolLock(`${ctx.session.id}:my_tool`);
|
|
59
|
+
* try {
|
|
60
|
+
* // ... tool body
|
|
61
|
+
* } finally {
|
|
62
|
+
* release();
|
|
63
|
+
* }
|
|
64
|
+
*/
|
|
65
|
+
export function acquireToolLock(key: string): () => void {
|
|
66
|
+
if (activeLocks.has(key)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`A call to this tool is already in progress for this session. Wait for it to complete before calling again.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
activeLocks.add(key);
|
|
72
|
+
return () => activeLocks.delete(key);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Bounded maps ────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Insert into a Map used as an LRU: the key moves to the back of the
|
|
79
|
+
* insertion order, and the oldest entries are evicted once `maxEntries` is
|
|
80
|
+
* exceeded. For isolate-lifetime caches keyed by unbounded request-derived
|
|
81
|
+
* ids (threads, rooms, events), the cap is what keeps them from growing for
|
|
82
|
+
* as long as the isolate lives.
|
|
83
|
+
*/
|
|
84
|
+
export function lruInsert<V>(
|
|
85
|
+
map: Map<string, V>,
|
|
86
|
+
key: string,
|
|
87
|
+
value: V,
|
|
88
|
+
maxEntries: number,
|
|
89
|
+
): void {
|
|
90
|
+
map.delete(key);
|
|
91
|
+
map.set(key, value);
|
|
92
|
+
while (map.size > maxEntries) {
|
|
93
|
+
const oldest = map.keys().next().value;
|
|
94
|
+
if (oldest === undefined) break;
|
|
95
|
+
map.delete(oldest);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drop expired entries from a TTL map. The runtime's small lookup caches
|
|
101
|
+
* check expiry lazily on read, which leaves entries for keys that are never
|
|
102
|
+
* read again resident forever; calling this on each write keeps such maps
|
|
103
|
+
* bounded by their active key set without a background timer.
|
|
104
|
+
*/
|
|
105
|
+
export function sweepExpired<V extends { expiresAt: number }>(
|
|
106
|
+
map: Map<string, V>,
|
|
107
|
+
now: number = Date.now(),
|
|
108
|
+
): void {
|
|
109
|
+
for (const [key, entry] of map) {
|
|
110
|
+
if (entry.expiresAt <= now) map.delete(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Client-declared tool schemas ────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
type JsonRecord = Record<string, unknown>;
|
|
117
|
+
|
|
118
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
119
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `#` and `#/$defs/*` / `#/definitions/*` are the only refs `z.fromJSONSchema`
|
|
124
|
+
* knows how to resolve — everything else it rejects outright.
|
|
125
|
+
*/
|
|
126
|
+
function isResolvableByZod(ref: string): boolean {
|
|
127
|
+
return ref === '#' || /^#\/(?:\$defs|definitions)\//.test(ref);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Decode the `~1` / `~0` escapes defined by RFC 6901. */
|
|
131
|
+
function decodeSegment(segment: string): string {
|
|
132
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolvePointer(root: JsonRecord, ref: string): unknown {
|
|
136
|
+
const segments = ref.slice(1).split('/').filter(Boolean).map(decodeSegment);
|
|
137
|
+
let node: unknown = root;
|
|
138
|
+
for (const segment of segments) {
|
|
139
|
+
if (Array.isArray(node)) {
|
|
140
|
+
const index = Number(segment);
|
|
141
|
+
if (!Number.isInteger(index)) return undefined;
|
|
142
|
+
node = node[index];
|
|
143
|
+
} else if (isRecord(node)) {
|
|
144
|
+
node = node[segment];
|
|
145
|
+
} else {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
if (node === undefined) return undefined;
|
|
149
|
+
}
|
|
150
|
+
return node;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Ceiling on how many pointers a single schema may expand. Inlining duplicates
|
|
155
|
+
* subtrees, so a schema built from many cross-referencing pointers can grow
|
|
156
|
+
* super-linearly; past this point the remaining refs are dropped rather than
|
|
157
|
+
* expanded. Far above what a real tool schema needs.
|
|
158
|
+
*/
|
|
159
|
+
const MAX_EXPANSIONS = 1_000;
|
|
160
|
+
|
|
161
|
+
interface InlineState {
|
|
162
|
+
/** Pointers currently being expanded — guards self-referential targets. */
|
|
163
|
+
active: Set<string>;
|
|
164
|
+
expansions: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function inline(node: unknown, root: JsonRecord, state: InlineState): unknown {
|
|
168
|
+
if (Array.isArray(node)) {
|
|
169
|
+
return node.map((item) => inline(item, root, state));
|
|
170
|
+
}
|
|
171
|
+
if (!isRecord(node)) return node;
|
|
172
|
+
|
|
173
|
+
const inlinedSiblings: JsonRecord = {};
|
|
174
|
+
for (const [key, value] of Object.entries(node)) {
|
|
175
|
+
if (key === '$ref') continue;
|
|
176
|
+
inlinedSiblings[key] = inline(value, root, state);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const ref = node.$ref;
|
|
180
|
+
if (typeof ref !== 'string') {
|
|
181
|
+
if ('$ref' in node) inlinedSiblings.$ref = ref;
|
|
182
|
+
return inlinedSiblings;
|
|
183
|
+
}
|
|
184
|
+
// Leave refs zod handles itself, and external refs (it raises its own,
|
|
185
|
+
// clearer error for those).
|
|
186
|
+
if (isResolvableByZod(ref) || !ref.startsWith('#')) {
|
|
187
|
+
inlinedSiblings.$ref = ref;
|
|
188
|
+
return inlinedSiblings;
|
|
189
|
+
}
|
|
190
|
+
// A pointer whose target contains the pointer would expand forever. Drop the
|
|
191
|
+
// constraint — an unconstrained value still validates.
|
|
192
|
+
if (state.active.has(ref)) return inlinedSiblings;
|
|
193
|
+
if (state.expansions >= MAX_EXPANSIONS) return inlinedSiblings;
|
|
194
|
+
|
|
195
|
+
const target = resolvePointer(root, ref);
|
|
196
|
+
if (!isRecord(target)) return inlinedSiblings;
|
|
197
|
+
|
|
198
|
+
state.expansions += 1;
|
|
199
|
+
state.active.add(ref);
|
|
200
|
+
const expanded = inline(target, root, state);
|
|
201
|
+
state.active.delete(ref);
|
|
202
|
+
|
|
203
|
+
return isRecord(expanded)
|
|
204
|
+
? { ...expanded, ...inlinedSiblings }
|
|
205
|
+
: inlinedSiblings;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Replace every local JSON-pointer `$ref` with the subschema it points at.
|
|
210
|
+
*
|
|
211
|
+
* `zod-to-json-schema` defaults to `$refStrategy: "root"`, so a subschema that
|
|
212
|
+
* appears more than once in a tool's schema is emitted once and every later
|
|
213
|
+
* occurrence becomes a pointer back into the document
|
|
214
|
+
* (`#/properties/ops/items/anyOf/2/properties/questionId`). Those pointers are
|
|
215
|
+
* valid JSON Schema but unresolvable by `z.fromJSONSchema`, so inline them
|
|
216
|
+
* before conversion.
|
|
217
|
+
*/
|
|
218
|
+
export function inlineLocalJsonPointerRefs(schema: JsonRecord): JsonRecord {
|
|
219
|
+
const inlined = inline(schema, schema, { active: new Set(), expansions: 0 });
|
|
220
|
+
return isRecord(inlined) ? inlined : schema;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Convert a client-declared JSON Schema (browser tool / AG-UI action) into a
|
|
225
|
+
* Zod schema.
|
|
226
|
+
*
|
|
227
|
+
* Returns `null` when the schema cannot be converted. Callers must skip that
|
|
228
|
+
* tool: these hooks run inside `collectRequest`, where one malformed client
|
|
229
|
+
* schema must not be allowed to take the request down with it.
|
|
230
|
+
*/
|
|
231
|
+
export function clientSchemaToZod(
|
|
232
|
+
schema: JsonRecord,
|
|
233
|
+
toolName: string,
|
|
234
|
+
logger: Logger = NOOP_LOGGER,
|
|
235
|
+
): z.ZodType | null {
|
|
236
|
+
try {
|
|
237
|
+
return z.fromJSONSchema(inlineLocalJsonPointerRefs(schema));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error(
|
|
240
|
+
`Skipping client-declared tool "${toolName}": its JSON Schema could not be converted to Zod — ${
|
|
241
|
+
error instanceof Error ? error.message : String(error)
|
|
242
|
+
}`,
|
|
243
|
+
);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import type { StructuredTool } from 'langchain';
|
|
3
|
+
import type {
|
|
4
|
+
PluginTool,
|
|
5
|
+
RuntimeContext,
|
|
6
|
+
SharedAccessors,
|
|
7
|
+
} from '../plugin-api/types';
|
|
8
|
+
import {
|
|
9
|
+
buildRuntimeContext,
|
|
10
|
+
type AmbientServices,
|
|
11
|
+
type RunConfig,
|
|
12
|
+
type RunConfigContext,
|
|
13
|
+
type RuntimeStateInput,
|
|
14
|
+
} from './runtime-context';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What `wrapPluginTool` needs at each call: the captured ambient bag, the
|
|
18
|
+
* current graph state, and the plugin title used for the description prefix.
|
|
19
|
+
*/
|
|
20
|
+
export interface WrapPluginToolOptions {
|
|
21
|
+
ambient: AmbientServices;
|
|
22
|
+
/** Snapshot of the graph state for the in-flight build. */
|
|
23
|
+
state: RuntimeStateInput;
|
|
24
|
+
/** Plugin manifest title used to auto-prefix the description. */
|
|
25
|
+
pluginTitle?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Builds the `ctx.shared` accessor bag for each call. The main-agent build
|
|
28
|
+
* wires the shared-state registry here; ad-hoc callers may omit it.
|
|
29
|
+
*/
|
|
30
|
+
sharedFactory?: (ctx: RuntimeContext) => SharedAccessors;
|
|
31
|
+
/**
|
|
32
|
+
* The request's user/session, used when the invocation config carries no
|
|
33
|
+
* `context` channel. The agent is built per request, so the build-time
|
|
34
|
+
* context IS this request's; a caller that also passes `context` to
|
|
35
|
+
* `invoke`/`stream` (the Node runtime does) overrides it.
|
|
36
|
+
*/
|
|
37
|
+
fallbackContext?: RunConfigContext;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the `RunConfig` a tool call sees: LangChain's `ToolRuntime` when
|
|
42
|
+
* it carries a usable `context`, else the build-time fallback.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveRunConfig(
|
|
45
|
+
runConfig: unknown,
|
|
46
|
+
fallbackContext: RunConfigContext | undefined,
|
|
47
|
+
): RunConfig {
|
|
48
|
+
const raw = (runConfig ?? {}) as Partial<RunConfig>;
|
|
49
|
+
const ctx = raw.context;
|
|
50
|
+
const usable = Boolean(ctx && ctx.user && ctx.session);
|
|
51
|
+
const context = usable ? ctx : fallbackContext;
|
|
52
|
+
if (!context) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'wrapPluginTool: no run context — pass `context: { user, session }` in the invoke config or supply `fallbackContext`.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return { ...raw, context };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bridge a `PluginTool` (handler signature `(args, ctx: RuntimeContext)`)
|
|
62
|
+
* into LangChain's `tool()` calling convention `(args, runConfig)`.
|
|
63
|
+
*
|
|
64
|
+
* Per-call, the wrapper synthesises a fresh `RuntimeContext` via
|
|
65
|
+
* `buildRuntimeContext` so handlers see the same shape regardless of where
|
|
66
|
+
* the call originates.
|
|
67
|
+
*
|
|
68
|
+
* The agent-facing description is auto-prefixed with the plugin's
|
|
69
|
+
* `manifest.title` so the agent always knows which plugin a tool belongs to.
|
|
70
|
+
*/
|
|
71
|
+
export function wrapPluginTool(
|
|
72
|
+
pluginTool: PluginTool,
|
|
73
|
+
options: WrapPluginToolOptions,
|
|
74
|
+
): StructuredTool {
|
|
75
|
+
const { ambient, state, pluginTitle, sharedFactory, fallbackContext } =
|
|
76
|
+
options;
|
|
77
|
+
const description = pluginTitle
|
|
78
|
+
? `[${pluginTitle}] ${pluginTool.description}`
|
|
79
|
+
: pluginTool.description;
|
|
80
|
+
|
|
81
|
+
return tool(
|
|
82
|
+
async (args, runConfig) => {
|
|
83
|
+
// LangChain passes the `ToolRuntime` as `runConfig`; its `context`
|
|
84
|
+
// channel carries user/session when the caller supplied one at
|
|
85
|
+
// invocation, otherwise the build-time request context applies.
|
|
86
|
+
const ctx = buildRuntimeContext(
|
|
87
|
+
resolveRunConfig(runConfig, fallbackContext),
|
|
88
|
+
ambient,
|
|
89
|
+
state,
|
|
90
|
+
sharedFactory,
|
|
91
|
+
);
|
|
92
|
+
return pluginTool.handler(args, ctx);
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: pluginTool.name,
|
|
96
|
+
description,
|
|
97
|
+
schema: pluginTool.schema,
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|