@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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT-subscription history sanitizer — a port of the Node runtime's
|
|
3
|
+
* `graph/middlewares/byo-history-sanitizer-middleware.ts`, installed on every
|
|
4
|
+
* turn (as on Node); the kwargs normalisation is gated to BYO ChatGPT turns via
|
|
5
|
+
* `runtime.context.byo`.
|
|
6
|
+
*/
|
|
7
|
+
import { AIMessage, type BaseMessage } from '@langchain/core/messages';
|
|
8
|
+
import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
9
|
+
import type { Logger } from '../../plugin-api/types';
|
|
10
|
+
import { NOOP_LOGGER } from '../utils';
|
|
11
|
+
|
|
12
|
+
export interface ByoHistorySanitizerMiddlewareOptions {
|
|
13
|
+
/** Optional logger; defaults to a no-op. */
|
|
14
|
+
logger?: Logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether this turn runs on the user's ChatGPT subscription — the only BYO
|
|
23
|
+
* provider on the Responses API, whose input converter chokes on reasoning
|
|
24
|
+
* kwargs written by other providers. Same narrow-don't-trust context read as
|
|
25
|
+
* the credits middleware.
|
|
26
|
+
*/
|
|
27
|
+
function isByoChatGptTurn(context: unknown): boolean {
|
|
28
|
+
if (!context || typeof context !== 'object' || !('byo' in context)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const { byo } = context;
|
|
32
|
+
return Boolean(
|
|
33
|
+
byo &&
|
|
34
|
+
typeof byo === 'object' &&
|
|
35
|
+
'active' in byo &&
|
|
36
|
+
byo.active === true &&
|
|
37
|
+
'provider' in byo &&
|
|
38
|
+
byo.provider === 'chatgpt',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Strip `{ type: 'reasoning' }` blocks out of array-shaped assistant
|
|
44
|
+
* content. ChatGPT (Responses API) turns stream thinking summaries as
|
|
45
|
+
* reasoning content blocks that end up in checkpointed state; they are
|
|
46
|
+
* display residue — reasoning continuity rides in
|
|
47
|
+
* `additional_kwargs.reasoning` — and no provider's request converter
|
|
48
|
+
* (including the Responses one) should be fed them back as input. Returns
|
|
49
|
+
* the original array when nothing needs stripping.
|
|
50
|
+
*/
|
|
51
|
+
function stripReasoningBlocks(
|
|
52
|
+
content: AIMessage['content'],
|
|
53
|
+
): AIMessage['content'] {
|
|
54
|
+
if (!Array.isArray(content)) return content;
|
|
55
|
+
const hasReasoningBlock = content.some(
|
|
56
|
+
(block) => isRecord(block) && block.type === 'reasoning',
|
|
57
|
+
);
|
|
58
|
+
if (!hasReasoningBlock) return content;
|
|
59
|
+
const kept = content.filter(
|
|
60
|
+
(block) => !(isRecord(block) && block.type === 'reasoning'),
|
|
61
|
+
);
|
|
62
|
+
return kept.length > 0 ? kept : '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize one assistant message's history representation before it is
|
|
67
|
+
* converted into a provider request:
|
|
68
|
+
*
|
|
69
|
+
* - reasoning content blocks are stripped on every turn (see
|
|
70
|
+
* {@link stripReasoningBlocks});
|
|
71
|
+
* - on BYO ChatGPT turns, `additional_kwargs.reasoning` must satisfy the
|
|
72
|
+
* Responses input converter, which requires `summary` to be an array
|
|
73
|
+
* whenever a reasoning item is forwarded: a well-formed item passes
|
|
74
|
+
* through, an item with `encrypted_content` but no `summary` (the shape
|
|
75
|
+
* this backend streams when summaries aren't requested) gets
|
|
76
|
+
* `summary: []` so the encrypted reasoning still round-trips, and
|
|
77
|
+
* anything else (e.g. OpenRouter's reasoning shape from platform turns
|
|
78
|
+
* earlier in the same thread) is dropped from the outbound copy. Other
|
|
79
|
+
* providers' converters ignore the kwargs, so they are left alone
|
|
80
|
+
* off-ChatGPT.
|
|
81
|
+
*
|
|
82
|
+
* Always returns a new message instance on change — checkpointed state is
|
|
83
|
+
* never mutated. Returns the original instance when nothing needs to change.
|
|
84
|
+
*/
|
|
85
|
+
function sanitizeMessage(
|
|
86
|
+
message: BaseMessage,
|
|
87
|
+
normalizeReasoningKwargs: boolean,
|
|
88
|
+
): BaseMessage {
|
|
89
|
+
if (!(message instanceof AIMessage)) return message;
|
|
90
|
+
|
|
91
|
+
const content = stripReasoningBlocks(message.content);
|
|
92
|
+
const contentChanged = content !== message.content;
|
|
93
|
+
|
|
94
|
+
const reasoning: unknown = message.additional_kwargs?.reasoning;
|
|
95
|
+
let kwargs = message.additional_kwargs;
|
|
96
|
+
let kwargsChanged = false;
|
|
97
|
+
if (
|
|
98
|
+
normalizeReasoningKwargs &&
|
|
99
|
+
reasoning !== undefined &&
|
|
100
|
+
!(isRecord(reasoning) && Array.isArray(reasoning.summary))
|
|
101
|
+
) {
|
|
102
|
+
const next: Record<string, unknown> = { ...message.additional_kwargs };
|
|
103
|
+
if (
|
|
104
|
+
isRecord(reasoning) &&
|
|
105
|
+
typeof reasoning.encrypted_content === 'string'
|
|
106
|
+
) {
|
|
107
|
+
next.reasoning = { ...reasoning, summary: [] };
|
|
108
|
+
} else {
|
|
109
|
+
delete next.reasoning;
|
|
110
|
+
}
|
|
111
|
+
kwargs = next;
|
|
112
|
+
kwargsChanged = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!contentChanged && !kwargsChanged) return message;
|
|
116
|
+
|
|
117
|
+
return new AIMessage({
|
|
118
|
+
content,
|
|
119
|
+
id: message.id,
|
|
120
|
+
name: message.name,
|
|
121
|
+
tool_calls: message.tool_calls,
|
|
122
|
+
invalid_tool_calls: message.invalid_tool_calls,
|
|
123
|
+
usage_metadata: message.usage_metadata,
|
|
124
|
+
response_metadata: message.response_metadata,
|
|
125
|
+
additional_kwargs: kwargs,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Rewrites the outbound message history so cross-provider residue can't
|
|
131
|
+
* poison a model request. A thread that mixes platform (OpenRouter) turns
|
|
132
|
+
* with BYO ChatGPT (Responses API) turns carries assistant messages whose
|
|
133
|
+
* reasoning payloads only one side understands: foreign reasoning kwargs
|
|
134
|
+
* crash the Responses input converter before the request is sent, and
|
|
135
|
+
* ChatGPT's reasoning content blocks are display residue no provider should
|
|
136
|
+
* be fed back. Block-stripping runs on every turn; kwargs normalization is
|
|
137
|
+
* gated to BYO ChatGPT turns. Untouched messages pass through by reference.
|
|
138
|
+
*/
|
|
139
|
+
export const createByoHistorySanitizerMiddleware = (
|
|
140
|
+
options?: ByoHistorySanitizerMiddlewareOptions,
|
|
141
|
+
): AgentMiddleware => {
|
|
142
|
+
const logger = options?.logger ?? NOOP_LOGGER;
|
|
143
|
+
|
|
144
|
+
return createMiddleware({
|
|
145
|
+
name: 'ByoHistorySanitizerMiddleware',
|
|
146
|
+
wrapModelCall: async (request, handler) => {
|
|
147
|
+
const chatGptTurn = isByoChatGptTurn(request.runtime.context);
|
|
148
|
+
|
|
149
|
+
let changed = 0;
|
|
150
|
+
const messages = request.messages.map((message) => {
|
|
151
|
+
const sanitized = sanitizeMessage(message, chatGptTurn);
|
|
152
|
+
if (sanitized !== message) changed += 1;
|
|
153
|
+
return sanitized;
|
|
154
|
+
});
|
|
155
|
+
if (changed === 0) {
|
|
156
|
+
return handler(request);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.log(
|
|
160
|
+
`[ByoHistorySanitizer] normalized reasoning history on ${changed} assistant message(s)`,
|
|
161
|
+
);
|
|
162
|
+
return handler({ ...request, messages });
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { Logger, PluginManifest } from '../../plugin-api/types';
|
|
4
|
+
import { NOOP_LOGGER } from '../utils';
|
|
5
|
+
|
|
6
|
+
type Visibility = NonNullable<PluginManifest['visibility']>;
|
|
7
|
+
|
|
8
|
+
export interface CapabilityGateMiddlewareOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Tool name → contributing plugin name. Tools absent from this map
|
|
11
|
+
* (meta-tools like `list_capabilities`/`load_capability`, ad-hoc tools
|
|
12
|
+
* registered outside the plugin pipeline) are always passed through.
|
|
13
|
+
*/
|
|
14
|
+
pluginByToolName: Map<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Tool name → effective visibility. Per-tool override wins over the
|
|
17
|
+
* plugin's manifest visibility; the caller pre-resolves this so the
|
|
18
|
+
* middleware does not need access to the registries at runtime.
|
|
19
|
+
*/
|
|
20
|
+
visibilityByToolName: Map<string, Visibility>;
|
|
21
|
+
/** Optional logger; defaults to a no-op. */
|
|
22
|
+
logger?: Logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gates on-demand plugin tools (and sub-agents-as-tools) per model call.
|
|
27
|
+
*
|
|
28
|
+
* All plugin tools are bound to the agent at compile time. This middleware
|
|
29
|
+
* runs on every model invocation: it reads `state.loadedPlugins` and trims
|
|
30
|
+
* the request's `tools` array down to what the agent should actually see at
|
|
31
|
+
* this point in the conversation.
|
|
32
|
+
*
|
|
33
|
+
* Why a middleware: `createAgent({ tools })` freezes the bound list, so
|
|
34
|
+
* `load_capability` updating state mid-run would otherwise have no effect
|
|
35
|
+
* until the next request rebuilt the agent. Filtering inside `wrapModelCall`
|
|
36
|
+
* lets a load decision take effect on the very next LLM call.
|
|
37
|
+
*/
|
|
38
|
+
export const createCapabilityGateMiddleware = (
|
|
39
|
+
options: CapabilityGateMiddlewareOptions,
|
|
40
|
+
): AgentMiddleware => {
|
|
41
|
+
const { pluginByToolName, visibilityByToolName } = options;
|
|
42
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
43
|
+
|
|
44
|
+
return createMiddleware({
|
|
45
|
+
name: 'CapabilityGateMiddleware',
|
|
46
|
+
stateSchema: z.object({
|
|
47
|
+
loadedPlugins: z.array(z.string()).optional(),
|
|
48
|
+
}),
|
|
49
|
+
// No `contextSchema`: the gate reads only `state.loadedPlugins`, and
|
|
50
|
+
// declaring one would make LangChain reject every invocation that omits
|
|
51
|
+
// `context` — the tool wrapper already carries the request context.
|
|
52
|
+
wrapModelCall: (request, handler) => {
|
|
53
|
+
const loaded = new Set<string>(request.state.loadedPlugins ?? []);
|
|
54
|
+
const filtered = request.tools.filter((t) => {
|
|
55
|
+
// LangChain types `tools` as `(ServerTool | ClientTool)[]`, where
|
|
56
|
+
// `ServerTool` is `Record<string, unknown>` — so `t.name` widens to
|
|
57
|
+
// `unknown`. Narrow at runtime; unknown-named tools pass through.
|
|
58
|
+
const name = typeof t.name === 'string' ? t.name : undefined;
|
|
59
|
+
if (!name) return true;
|
|
60
|
+
const plugin = pluginByToolName.get(name);
|
|
61
|
+
if (!plugin) return true;
|
|
62
|
+
const viz = visibilityByToolName.get(name) ?? 'on-demand';
|
|
63
|
+
if (viz === 'always' || viz === 'silent') return true;
|
|
64
|
+
return loaded.has(plugin);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (filtered.length !== request.tools.length) {
|
|
68
|
+
logger.log(
|
|
69
|
+
`[CapabilityGateMiddleware] exposed ${filtered.length}/${request.tools.length} tools; loadedPlugins=${Array.from(loaded).join(',') || '∅'}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return handler({ ...request, tools: filtered });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { createCapabilityGateMiddleware } from './capability-gate';
|
|
2
|
+
export type { CapabilityGateMiddlewareOptions } from './capability-gate';
|
|
3
|
+
export { createToolValidationMiddleware } from './tool-validation';
|
|
4
|
+
export type { ToolValidationMiddlewareOptions } from './tool-validation';
|
|
5
|
+
export { createToolRepetitionGuardMiddleware } from './tool-repetition-guard';
|
|
6
|
+
export type { ToolRepetitionGuardMiddlewareOptions } from './tool-repetition-guard';
|
|
7
|
+
export {
|
|
8
|
+
createSummarizationMiddleware,
|
|
9
|
+
isSummarizationMessage,
|
|
10
|
+
SUMMARY_PREFIX,
|
|
11
|
+
} from './summarization';
|
|
12
|
+
export type { SummarizationMiddlewareOptions } from './summarization';
|
|
13
|
+
export { createByoHistorySanitizerMiddleware } from './byo-history-sanitizer';
|
|
14
|
+
export type { ByoHistorySanitizerMiddlewareOptions } from './byo-history-sanitizer';
|
|
15
|
+
export { createPageContextMiddleware } from './page-context';
|
|
16
|
+
export type { PageContextMiddlewareOptions } from './page-context';
|
|
17
|
+
export { createSafetyGuardrailMiddleware } from './safety-guardrail';
|
|
18
|
+
export type { SafetyGuardrailMiddlewareOptions } from './safety-guardrail';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active-page context — a port of the Node runtime's
|
|
3
|
+
* `graph/middlewares/page-context-middleware.ts`. Installed only when the host
|
|
4
|
+
* supplies `hooks.getRoomTitle` (as on Node).
|
|
5
|
+
*/
|
|
6
|
+
import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import type { Logger } from '../../plugin-api/types';
|
|
9
|
+
import { NOOP_LOGGER } from '../utils';
|
|
10
|
+
|
|
11
|
+
export interface PageContextMiddlewareOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Resolves a human-readable page title for the given editor room ID.
|
|
14
|
+
* Returns `undefined` if the room has no title or the lookup fails — the
|
|
15
|
+
* middleware will fall back to using the bare room ID.
|
|
16
|
+
*
|
|
17
|
+
* In the IXO oracle this is backed by a Matrix `m.room.name` state event;
|
|
18
|
+
* pass any equivalent lookup that fits your storage.
|
|
19
|
+
*/
|
|
20
|
+
getRoomTitle: (roomId: string) => Promise<string | undefined>;
|
|
21
|
+
/** Optional logger; defaults to a no-op. */
|
|
22
|
+
logger?: Logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatLabel(title: string | undefined, roomId: string): string {
|
|
26
|
+
return title ? `"${title}" (${roomId})` : roomId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Injects a short "Active Page Context" block into the system message when
|
|
31
|
+
* the agent is editing a page (`state.editorRoomId` is set). When the user
|
|
32
|
+
* switches pages mid-conversation the block flags it explicitly so the
|
|
33
|
+
* agent re-reads the current page before editing.
|
|
34
|
+
*/
|
|
35
|
+
export const createPageContextMiddleware = (
|
|
36
|
+
options: PageContextMiddlewareOptions,
|
|
37
|
+
): AgentMiddleware => {
|
|
38
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
39
|
+
const { getRoomTitle } = options;
|
|
40
|
+
|
|
41
|
+
return createMiddleware({
|
|
42
|
+
name: 'PageContextMiddleware',
|
|
43
|
+
stateSchema: z.object({
|
|
44
|
+
editorRoomId: z.string().optional(),
|
|
45
|
+
_previousEditorRoomId: z.string().optional(),
|
|
46
|
+
}),
|
|
47
|
+
wrapModelCall: async (request, handler) => {
|
|
48
|
+
const currentEditorRoomId = request.state.editorRoomId;
|
|
49
|
+
if (!currentEditorRoomId) {
|
|
50
|
+
return handler(request);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const currentTitle = await getRoomTitle(currentEditorRoomId);
|
|
54
|
+
const currentLabel = formatLabel(currentTitle, currentEditorRoomId);
|
|
55
|
+
const previousEditorRoomId = request.state._previousEditorRoomId;
|
|
56
|
+
|
|
57
|
+
let pageContext: string;
|
|
58
|
+
if (
|
|
59
|
+
previousEditorRoomId &&
|
|
60
|
+
previousEditorRoomId !== currentEditorRoomId
|
|
61
|
+
) {
|
|
62
|
+
logger.log(
|
|
63
|
+
`[PageContextMiddleware] Page switch: ${previousEditorRoomId} → ${currentEditorRoomId}`,
|
|
64
|
+
);
|
|
65
|
+
const previousTitle = await getRoomTitle(previousEditorRoomId);
|
|
66
|
+
const previousLabel = formatLabel(previousTitle, previousEditorRoomId);
|
|
67
|
+
|
|
68
|
+
pageContext =
|
|
69
|
+
`\n\n## 📄 Active Page Context\n\n` +
|
|
70
|
+
`The user has switched pages. Current page: ${currentLabel}. ` +
|
|
71
|
+
`Previous page: ${previousLabel}. ` +
|
|
72
|
+
`Previous page context in conversation history may be stale. ` +
|
|
73
|
+
`Always favour the current active page. ` +
|
|
74
|
+
`Before making any edits, read the current page's content to confirm it ` +
|
|
75
|
+
`matches what the user is asking you to work on. ` +
|
|
76
|
+
`If the content differs from what was discussed, confirm with the user before editing.`;
|
|
77
|
+
} else {
|
|
78
|
+
pageContext =
|
|
79
|
+
`\n\n## 📄 Active Page Context\n\n` +
|
|
80
|
+
`Current active page: ${currentLabel}. Always work with this page.`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return handler({
|
|
84
|
+
...request,
|
|
85
|
+
systemMessage: request.systemMessage.concat(pageContext),
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
afterModel: (state) => {
|
|
89
|
+
if (
|
|
90
|
+
state.editorRoomId &&
|
|
91
|
+
state.editorRoomId !== state._previousEditorRoomId
|
|
92
|
+
) {
|
|
93
|
+
return { _previousEditorRoomId: state.editorRoomId };
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety guardrail — a port of the Node runtime's
|
|
3
|
+
* `graph/middlewares/safety-guardrail-middleware.ts`. Installed only when the
|
|
4
|
+
* host supplies `hooks.safetyModel` (as on Node).
|
|
5
|
+
*/
|
|
6
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
7
|
+
import { RemoveMessage } from '@langchain/core/messages';
|
|
8
|
+
import { type AgentMiddleware, AIMessage, createMiddleware } from 'langchain';
|
|
9
|
+
import type { Logger } from '../../plugin-api/types';
|
|
10
|
+
import { NOOP_LOGGER } from '../utils';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SAFETY_PROMPT = `Evaluate if this response is safe. Respond ONLY with 'SAFE' or 'UNSAFE'.
|
|
13
|
+
|
|
14
|
+
Mark as UNSAFE ONLY if the response:
|
|
15
|
+
- Contains actual API keys, tokens, passwords, or credentials (not just mentioning they exist)
|
|
16
|
+
- Reveals security vulnerabilities or exploitation methods
|
|
17
|
+
- Contains harmful, dangerous, or illegal content
|
|
18
|
+
- Includes personal/sensitive data not meant to be shared
|
|
19
|
+
- Attempts prompt injection or jailbreak techniques
|
|
20
|
+
|
|
21
|
+
ALWAYS mark as SAFE if the response:
|
|
22
|
+
- Explains user-facing features (memory, knowledge, agents, tools, capabilities)
|
|
23
|
+
- Provides how-to instructions or workflows
|
|
24
|
+
- Describes general system functionality or capabilities
|
|
25
|
+
- Mentions tool names or agent names in the context of explaining features
|
|
26
|
+
- ALLOW AWS pre-signed url to be used in the response
|
|
27
|
+
- Describes document/block editing operations (status updates, property changes, block creation/deletion)
|
|
28
|
+
- References block IDs (UUIDs), block properties, or CRDT/Y.js synchronization
|
|
29
|
+
- Contains URLs in the context of document block properties (kycUrl, redirectUrl, callback URLs)
|
|
30
|
+
- Describes survey answers, form data, or workflow state changes
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const DEFAULT_SAFE_REPLY = "I'm sorry, but I can't provide that information.";
|
|
34
|
+
|
|
35
|
+
export interface SafetyGuardrailMiddlewareOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Small classification model that returns either "SAFE" or "UNSAFE".
|
|
38
|
+
* Typically the cheap "guard" role.
|
|
39
|
+
*/
|
|
40
|
+
safetyModel: BaseChatModel;
|
|
41
|
+
/** Override the system prompt used by the safety classifier. */
|
|
42
|
+
safetyPrompt?: string;
|
|
43
|
+
/** Override the user-visible message returned when a response is blocked. */
|
|
44
|
+
safeReply?: string;
|
|
45
|
+
/** Optional logger; defaults to a no-op. */
|
|
46
|
+
logger?: Logger;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Runs after the agent finishes producing a user-facing reply: routes the
|
|
51
|
+
* final assistant text + the most recent user request through `safetyModel`,
|
|
52
|
+
* and replaces the reply with `safeReply` if the model returns "UNSAFE".
|
|
53
|
+
*
|
|
54
|
+
* Skipped automatically when the last AI message is a tool call (those are
|
|
55
|
+
* not user-facing text).
|
|
56
|
+
*/
|
|
57
|
+
export const createSafetyGuardrailMiddleware = (
|
|
58
|
+
options: SafetyGuardrailMiddlewareOptions,
|
|
59
|
+
): AgentMiddleware => {
|
|
60
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
61
|
+
const safetyPrompt = options.safetyPrompt ?? DEFAULT_SAFETY_PROMPT;
|
|
62
|
+
const safeReply = options.safeReply ?? DEFAULT_SAFE_REPLY;
|
|
63
|
+
const { safetyModel } = options;
|
|
64
|
+
|
|
65
|
+
return createMiddleware({
|
|
66
|
+
name: 'SafetyGuardrailMiddleware',
|
|
67
|
+
afterAgent: {
|
|
68
|
+
canJumpTo: ['end'],
|
|
69
|
+
hook: async (state) => {
|
|
70
|
+
if (!state.messages || state.messages.length === 0) return;
|
|
71
|
+
|
|
72
|
+
const lastMessage = state.messages[state.messages.length - 1];
|
|
73
|
+
if (lastMessage.type !== 'ai') return;
|
|
74
|
+
|
|
75
|
+
const aiMessage = lastMessage as AIMessage;
|
|
76
|
+
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) return;
|
|
77
|
+
|
|
78
|
+
const lastUserMessage = [...state.messages]
|
|
79
|
+
.reverse()
|
|
80
|
+
.find((message) => message.type === 'human');
|
|
81
|
+
const userContent = lastUserMessage
|
|
82
|
+
? lastUserMessage.content.toString()
|
|
83
|
+
: 'N/A';
|
|
84
|
+
|
|
85
|
+
const result = await safetyModel.invoke([
|
|
86
|
+
{ role: 'system', content: safetyPrompt },
|
|
87
|
+
{
|
|
88
|
+
role: 'user',
|
|
89
|
+
content: `User request: ${userContent}
|
|
90
|
+
+--------------------------------+
|
|
91
|
+
Assistant response: ${String(lastMessage.content)}
|
|
92
|
+
+--------------------------------+
|
|
93
|
+
Decision:`,
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const safetyDecision = String(result.content).trim().toUpperCase();
|
|
98
|
+
logger.log(`Safety decision: ${safetyDecision}`);
|
|
99
|
+
|
|
100
|
+
if (safetyDecision.includes('UNSAFE')) {
|
|
101
|
+
logger.warn(
|
|
102
|
+
'Unsafe response detected, blocking and returning safe message',
|
|
103
|
+
);
|
|
104
|
+
return {
|
|
105
|
+
messages: [
|
|
106
|
+
new RemoveMessage({ id: lastMessage.id ?? '' }),
|
|
107
|
+
new AIMessage(safeReply),
|
|
108
|
+
],
|
|
109
|
+
jumpTo: 'end',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return;
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
2
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import { type AgentMiddleware, summarizationMiddleware } from 'langchain';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IXO-flavoured prompt instructing the summarizer to preserve identifiers
|
|
7
|
+
* verbatim (DIDs, room IDs, addresses, hashes, URLs, etc.). Replaces the
|
|
8
|
+
* default LangChain summary prompt without changing the wider trigger logic.
|
|
9
|
+
*/
|
|
10
|
+
const SUMMARY_PROMPT = `<role>
|
|
11
|
+
Context Extraction Assistant
|
|
12
|
+
</role>
|
|
13
|
+
|
|
14
|
+
<primary_objective>
|
|
15
|
+
Extract the most important context from the conversation history below. This extracted context will REPLACE the full conversation history, so capture everything the agent needs to continue working effectively.
|
|
16
|
+
</primary_objective>
|
|
17
|
+
|
|
18
|
+
<critical_identifiers>
|
|
19
|
+
You MUST preserve ALL of the following VERBATIM — copy them exactly as they appear:
|
|
20
|
+
- DIDs (decentralized identifiers, e.g. did:ixo:..., did:x:..., did:key:...)
|
|
21
|
+
- Matrix Room IDs (e.g. !abc123:matrix.ixo.world)
|
|
22
|
+
- Wallet/account addresses (e.g. ixo1..., cosmos1...)
|
|
23
|
+
- Session IDs, thread IDs, or checkpoint IDs
|
|
24
|
+
- Blockchain transaction hashes or entity IDs
|
|
25
|
+
- Any URLs, endpoints, or API paths referenced
|
|
26
|
+
- Block IDs (UUIDs) from document editing
|
|
27
|
+
Do NOT paraphrase, abbreviate, or omit any of these identifiers.
|
|
28
|
+
</critical_identifiers>
|
|
29
|
+
|
|
30
|
+
<what_to_extract>
|
|
31
|
+
1. **Active task**: What is the agent currently working on? What was the user's most recent request?
|
|
32
|
+
2. **Key decisions & outcomes**: What has been decided or completed so far?
|
|
33
|
+
3. **Pending actions**: What still needs to be done?
|
|
34
|
+
4. **Important data**: Any structured data, configurations, or parameters the agent was working with
|
|
35
|
+
5. **All identifiers**: Every DID, room ID, address, hash listed above — verbatim
|
|
36
|
+
6. **Tool results**: Key outputs from tool calls that inform next steps
|
|
37
|
+
7. **Errors or blockers**: Any issues encountered that are still relevant
|
|
38
|
+
</what_to_extract>
|
|
39
|
+
|
|
40
|
+
<format>
|
|
41
|
+
Structure the extracted context clearly with sections. Be concise but complete — the agent will lose all context not captured here.
|
|
42
|
+
Respond ONLY with the extracted context. No preamble, no explanation.
|
|
43
|
+
</format>
|
|
44
|
+
|
|
45
|
+
<messages>
|
|
46
|
+
Messages to summarize:
|
|
47
|
+
{messages}
|
|
48
|
+
</messages>`;
|
|
49
|
+
|
|
50
|
+
export const SUMMARY_PREFIX = 'Here is a summary of the conversation to date:';
|
|
51
|
+
|
|
52
|
+
const DEFAULT_TRIGGER_MESSAGES = 20;
|
|
53
|
+
const DEFAULT_TRIGGER_TOKENS = 40_000;
|
|
54
|
+
const DEFAULT_KEEP_MESSAGES = 10;
|
|
55
|
+
|
|
56
|
+
export interface SummarizationMiddlewareOptions {
|
|
57
|
+
/** Model used to generate the summary (typically a small/cheap router model). */
|
|
58
|
+
model: BaseChatModel;
|
|
59
|
+
/** Override the trigger threshold for messages (default: 20). */
|
|
60
|
+
triggerMessages?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Also trigger on approximate token count (default: 40k). Message-count
|
|
63
|
+
* triggers alone miss threads whose few messages carry huge tool outputs.
|
|
64
|
+
*/
|
|
65
|
+
triggerTokens?: number;
|
|
66
|
+
/** Override the number of recent messages to keep (default: 10). */
|
|
67
|
+
keepMessages?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `true` for the condensed-history message the summarization middleware
|
|
72
|
+
* writes into graph state. List endpoints use this to keep it out of the
|
|
73
|
+
* user-visible transcript.
|
|
74
|
+
*/
|
|
75
|
+
export function isSummarizationMessage(message: BaseMessage): boolean {
|
|
76
|
+
if (message.additional_kwargs?.lc_source === 'summarization') return true;
|
|
77
|
+
const { content } = message;
|
|
78
|
+
return typeof content === 'string' && content.startsWith(SUMMARY_PREFIX);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wraps LangChain's built-in `summarizationMiddleware` with the IXO-specific
|
|
83
|
+
* summary prompt + prefix. Pass any compatible chat model — typically the
|
|
84
|
+
* cheap "routing" role.
|
|
85
|
+
*/
|
|
86
|
+
export const createSummarizationMiddleware = (
|
|
87
|
+
options: SummarizationMiddlewareOptions,
|
|
88
|
+
): AgentMiddleware => {
|
|
89
|
+
return summarizationMiddleware({
|
|
90
|
+
model: options.model,
|
|
91
|
+
summaryPrompt: SUMMARY_PROMPT,
|
|
92
|
+
summaryPrefix: SUMMARY_PREFIX,
|
|
93
|
+
trigger: [
|
|
94
|
+
{ messages: options.triggerMessages ?? DEFAULT_TRIGGER_MESSAGES },
|
|
95
|
+
{ tokens: options.triggerTokens ?? DEFAULT_TRIGGER_TOKENS },
|
|
96
|
+
],
|
|
97
|
+
keep: { messages: options.keepMessages ?? DEFAULT_KEEP_MESSAGES },
|
|
98
|
+
});
|
|
99
|
+
};
|