@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,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIMessage,
|
|
3
|
+
type BaseMessage,
|
|
4
|
+
ToolMessage,
|
|
5
|
+
} from '@langchain/core/messages';
|
|
6
|
+
import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
7
|
+
import type { Logger } from '../../plugin-api/types';
|
|
8
|
+
import { NOOP_LOGGER } from '../utils';
|
|
9
|
+
|
|
10
|
+
export interface ToolRepetitionGuardMiddlewareOptions {
|
|
11
|
+
/**
|
|
12
|
+
* How many messages back to scan for a prior identical failed call.
|
|
13
|
+
* Default 20 — enough to catch in-turn loops without trawling whole histories.
|
|
14
|
+
*/
|
|
15
|
+
lookback?: number;
|
|
16
|
+
/** Optional logger; defaults to a no-op. */
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detects when the agent issues the same `(toolName, args)` pair after that
|
|
22
|
+
* exact call already failed inside the recent window, and short-circuits
|
|
23
|
+
* before the tool is invoked again. The short-circuit message quotes the
|
|
24
|
+
* earlier error and tells the model to change tools/args instead.
|
|
25
|
+
*
|
|
26
|
+
* `toolRetryMiddleware` retries inside one call; this guard prevents the
|
|
27
|
+
* model from making the *next* identical call.
|
|
28
|
+
*/
|
|
29
|
+
export const createToolRepetitionGuardMiddleware = (
|
|
30
|
+
options: ToolRepetitionGuardMiddlewareOptions = {},
|
|
31
|
+
): AgentMiddleware => {
|
|
32
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
33
|
+
const lookback = options.lookback ?? 20;
|
|
34
|
+
|
|
35
|
+
return createMiddleware({
|
|
36
|
+
name: 'ToolRepetitionGuardMiddleware',
|
|
37
|
+
wrapToolCall: async (toolCallRequest, handler) => {
|
|
38
|
+
const { toolCall, state } = toolCallRequest;
|
|
39
|
+
const toolName = toolCall.name ?? toolCallRequest.tool?.name;
|
|
40
|
+
if (!toolName) return handler(toolCallRequest);
|
|
41
|
+
|
|
42
|
+
const argsKey = canonicalArgsKey(toolCall.args);
|
|
43
|
+
const messages = state.messages ?? [];
|
|
44
|
+
const start = Math.max(0, messages.length - lookback);
|
|
45
|
+
|
|
46
|
+
for (let i = messages.length - 1; i >= start; i--) {
|
|
47
|
+
const msg = messages[i];
|
|
48
|
+
if (!(msg instanceof ToolMessage)) continue;
|
|
49
|
+
if (msg.status !== 'error') continue;
|
|
50
|
+
if (msg.name !== toolName) continue;
|
|
51
|
+
|
|
52
|
+
const priorArgs = findToolCallArgsById(messages, msg.tool_call_id);
|
|
53
|
+
if (priorArgs === undefined) continue;
|
|
54
|
+
if (canonicalArgsKey(priorArgs) !== argsKey) continue;
|
|
55
|
+
|
|
56
|
+
const priorError = toolMessageText(msg);
|
|
57
|
+
logger.warn(
|
|
58
|
+
`Repetition guard: short-circuiting duplicate failed call to ${toolName}`,
|
|
59
|
+
{ toolName, priorError },
|
|
60
|
+
);
|
|
61
|
+
return new ToolMessage({
|
|
62
|
+
content: [
|
|
63
|
+
`You already called \`${toolName}\` with these exact arguments earlier in this turn and it failed with:`,
|
|
64
|
+
'',
|
|
65
|
+
priorError,
|
|
66
|
+
'',
|
|
67
|
+
'Do NOT repeat the same call. Read the error message, then either:',
|
|
68
|
+
'- change the arguments to satisfy the constraint the error describes, OR',
|
|
69
|
+
'- pick a different tool that fits the constraint.',
|
|
70
|
+
].join('\n'),
|
|
71
|
+
tool_call_id: toolCall.id ?? '',
|
|
72
|
+
name: toolName,
|
|
73
|
+
status: 'error',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return handler(toolCallRequest);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function canonicalArgsKey(args: unknown): string {
|
|
83
|
+
if (args === null || args === undefined) return 'null';
|
|
84
|
+
if (typeof args !== 'object') return JSON.stringify(args);
|
|
85
|
+
if (Array.isArray(args)) {
|
|
86
|
+
return `[${args.map(canonicalArgsKey).join(',')}]`;
|
|
87
|
+
}
|
|
88
|
+
const obj = args as Record<string, unknown>;
|
|
89
|
+
const keys = Object.keys(obj).sort();
|
|
90
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalArgsKey(obj[k])}`).join(',')}}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toolMessageText(msg: ToolMessage): string {
|
|
94
|
+
const content = msg.content;
|
|
95
|
+
if (typeof content === 'string') return content;
|
|
96
|
+
if (Array.isArray(content)) {
|
|
97
|
+
return content
|
|
98
|
+
.map((block) => {
|
|
99
|
+
if (typeof block === 'string') return block;
|
|
100
|
+
if (block && typeof block === 'object' && 'text' in block) {
|
|
101
|
+
const text = (block as { text?: unknown }).text;
|
|
102
|
+
return typeof text === 'string' ? text : '';
|
|
103
|
+
}
|
|
104
|
+
return '';
|
|
105
|
+
})
|
|
106
|
+
.join('\n');
|
|
107
|
+
}
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function findToolCallArgsById(
|
|
112
|
+
messages: BaseMessage[],
|
|
113
|
+
callId: string | undefined,
|
|
114
|
+
): unknown {
|
|
115
|
+
if (!callId) return undefined;
|
|
116
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
117
|
+
const m = messages[i];
|
|
118
|
+
if (!AIMessage.isInstance(m)) continue;
|
|
119
|
+
const toolCalls = m.tool_calls;
|
|
120
|
+
if (!toolCalls) continue;
|
|
121
|
+
const hit = toolCalls.find((tc) => tc.id === callId);
|
|
122
|
+
if (hit) return hit.args;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
import { type AgentMiddleware, createMiddleware, ToolMessage } from 'langchain';
|
|
3
|
+
import type { Logger } from '../../plugin-api/types';
|
|
4
|
+
import { NOOP_LOGGER } from '../utils';
|
|
5
|
+
|
|
6
|
+
export interface ToolValidationMiddlewareOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Tool names whose dangling `ToolMessage` outputs should be stripped before
|
|
9
|
+
* the next model call. These are typically sub-agent tools whose responses
|
|
10
|
+
* are summarised by their parent agent rather than being interpreted by the
|
|
11
|
+
* model directly.
|
|
12
|
+
*/
|
|
13
|
+
skipToolNames?: string[];
|
|
14
|
+
/** Optional logger; defaults to a no-op. */
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Catches tool validation errors (Zod / schema mismatches) and turns them
|
|
20
|
+
* into a `ToolMessage` describing the failure, so the model can recover
|
|
21
|
+
* instead of the graph crashing.
|
|
22
|
+
*
|
|
23
|
+
* Also strips `ToolMessage`s for tools listed in `skipToolNames` from the
|
|
24
|
+
* state before the next model call.
|
|
25
|
+
*/
|
|
26
|
+
export const createToolValidationMiddleware = (
|
|
27
|
+
options: ToolValidationMiddlewareOptions = {},
|
|
28
|
+
): AgentMiddleware => {
|
|
29
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
30
|
+
const skipToolNames = new Set(options.skipToolNames ?? []);
|
|
31
|
+
|
|
32
|
+
return createMiddleware({
|
|
33
|
+
name: 'ToolValidationMiddleware',
|
|
34
|
+
wrapToolCall: async (toolCallRequest, handler) => {
|
|
35
|
+
const { toolCall } = toolCallRequest;
|
|
36
|
+
try {
|
|
37
|
+
return await handler(toolCallRequest);
|
|
38
|
+
} catch (error: unknown) {
|
|
39
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
40
|
+
const isSchemaError =
|
|
41
|
+
errorMessage.includes('did not match expected schema') ||
|
|
42
|
+
errorMessage.includes('Received tool input did not match') ||
|
|
43
|
+
errorMessage.includes('schema') ||
|
|
44
|
+
(error instanceof Error && error.name === 'ZodError');
|
|
45
|
+
|
|
46
|
+
if (!isSchemaError) throw error;
|
|
47
|
+
|
|
48
|
+
const toolName = toolCall.name ?? toolCallRequest.tool?.name ?? '';
|
|
49
|
+
logger.warn(`Tool validation error for ${toolName}: ${errorMessage}`, {
|
|
50
|
+
toolName,
|
|
51
|
+
toolArgs: toolCall.args,
|
|
52
|
+
error: errorMessage,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new ToolMessage({
|
|
56
|
+
content: `Error: The tool "${toolName}" was called with invalid parameters. ${errorMessage}. Please check the tool's required parameters and try again with the correct format.`,
|
|
57
|
+
tool_call_id: toolCall.id ?? '',
|
|
58
|
+
name: toolName,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
beforeModel(state) {
|
|
63
|
+
if (skipToolNames.size === 0) return state;
|
|
64
|
+
|
|
65
|
+
const toolMessage = state.messages.find(
|
|
66
|
+
(message: BaseMessage) => message.type === 'tool',
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (toolMessage?.name && skipToolNames.has(toolMessage.name)) {
|
|
70
|
+
logger.log(
|
|
71
|
+
`Tool validation middleware: ${toolMessage.name} is a skip-listed tool, removing tool messages before next model call`,
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
messages: state.messages.filter(
|
|
76
|
+
(message: BaseMessage) => message.type !== 'tool',
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return state;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the per-turn `work_status` liveness card from inside the graph —
|
|
3
|
+
* the port of the Node runtime's `WorkStatusMiddleware`: one `Step n ·
|
|
4
|
+
* Thinking…` per model call and one `Step n · <Tool name>…` per tool call,
|
|
5
|
+
* read off the run context's `session.requestId`. A pure side effect: it
|
|
6
|
+
* returns the handler's result verbatim and never throws on its own.
|
|
7
|
+
*
|
|
8
|
+
* Turns that were not registered with the producer (HTTP turns, task runs)
|
|
9
|
+
* are no-ops, so the middleware is safe to install unconditionally.
|
|
10
|
+
*/
|
|
11
|
+
import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
12
|
+
import {
|
|
13
|
+
humanizeToolLabel,
|
|
14
|
+
type WorkStatusProducer,
|
|
15
|
+
} from '../../matrix/work-status';
|
|
16
|
+
|
|
17
|
+
const THINKING_LABEL = 'Thinking…';
|
|
18
|
+
|
|
19
|
+
export interface WorkStatusMiddlewareOptions {
|
|
20
|
+
producer: Pick<WorkStatusProducer, 'step'>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readRequestId(context: unknown): string | undefined {
|
|
24
|
+
if (typeof context !== 'object' || context === null) return undefined;
|
|
25
|
+
if (!('session' in context)) return undefined;
|
|
26
|
+
const session: unknown = context.session;
|
|
27
|
+
if (typeof session !== 'object' || session === null) return undefined;
|
|
28
|
+
if (!('requestId' in session)) return undefined;
|
|
29
|
+
const requestId: unknown = session.requestId;
|
|
30
|
+
return typeof requestId === 'string' && requestId.length > 0
|
|
31
|
+
? requestId
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createWorkStatusMiddleware(
|
|
36
|
+
options: WorkStatusMiddlewareOptions,
|
|
37
|
+
): AgentMiddleware {
|
|
38
|
+
const { producer } = options;
|
|
39
|
+
return createMiddleware({
|
|
40
|
+
name: 'WorkStatusMiddleware',
|
|
41
|
+
wrapModelCall: (request, handler) => {
|
|
42
|
+
const requestId = readRequestId(request.runtime?.context);
|
|
43
|
+
if (requestId) producer.step(requestId, THINKING_LABEL);
|
|
44
|
+
return handler(request);
|
|
45
|
+
},
|
|
46
|
+
wrapToolCall: (toolCallRequest, handler) => {
|
|
47
|
+
const requestId = readRequestId(toolCallRequest.runtime?.context);
|
|
48
|
+
if (requestId) {
|
|
49
|
+
producer.step(
|
|
50
|
+
requestId,
|
|
51
|
+
humanizeToolLabel(toolCallRequest.toolCall.name),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return handler(toolCallRequest);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live OpenRouter list prices for the model catalog — the Workers port of the
|
|
3
|
+
* Node runtime's `llm/openrouter-pricing.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Fetched from OpenRouter's public models API and cached per isolate for an
|
|
6
|
+
* hour. The catalog carries baseline prices so `GET /models` always answers;
|
|
7
|
+
* this module upgrades them to live numbers when the call succeeds and
|
|
8
|
+
* silently keeps serving the last good map (or nothing, so the baseline
|
|
9
|
+
* applies) when it doesn't. Prices returned here are RAW provider list
|
|
10
|
+
* prices; the markup is applied by `buildModelListing`.
|
|
11
|
+
*
|
|
12
|
+
* Per isolate rather than per process: a cold isolate pays one ~100 ms fetch
|
|
13
|
+
* on its first `/models`, then serves from memory until evicted.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import type { ModelPrice } from './llm';
|
|
17
|
+
|
|
18
|
+
export const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
19
|
+
export const OPENROUTER_PRICE_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* OpenRouter returns `pricing.prompt` / `pricing.completion` as strings in USD
|
|
23
|
+
* **per token** (e.g. `"0.0000002"`). Only those two fields are read, so a
|
|
24
|
+
* schema change upstream can't break the listing.
|
|
25
|
+
*/
|
|
26
|
+
const openRouterModelsSchema = z.object({
|
|
27
|
+
data: z.array(
|
|
28
|
+
z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
pricing: z
|
|
31
|
+
.object({
|
|
32
|
+
prompt: z.string().optional(),
|
|
33
|
+
completion: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
.optional(),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
interface PriceCache {
|
|
41
|
+
fetchedAt: number;
|
|
42
|
+
prices: Map<string, ModelPrice>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cache: PriceCache | null = null;
|
|
46
|
+
|
|
47
|
+
/** Convert an OpenRouter `$/token` string to `$/million tokens`, or `null`. */
|
|
48
|
+
function perMillion(pricePerToken: string | undefined): number | null {
|
|
49
|
+
if (pricePerToken == null) return null;
|
|
50
|
+
const perToken = Number(pricePerToken);
|
|
51
|
+
if (!Number.isFinite(perToken)) return null;
|
|
52
|
+
return perToken * 1_000_000;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parsePrices(
|
|
56
|
+
data: z.infer<typeof openRouterModelsSchema>['data'],
|
|
57
|
+
): Map<string, ModelPrice> {
|
|
58
|
+
const out = new Map<string, ModelPrice>();
|
|
59
|
+
for (const model of data) {
|
|
60
|
+
const inputPerMillion = perMillion(model.pricing?.prompt);
|
|
61
|
+
const outputPerMillion = perMillion(model.pricing?.completion);
|
|
62
|
+
if (inputPerMillion == null || outputPerMillion == null) continue;
|
|
63
|
+
out.set(model.id, { inputPerMillion, outputPerMillion });
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface FetchOpenRouterPricesOptions {
|
|
69
|
+
/** Sent as a bearer when present (higher rate limits); the endpoint is public. */
|
|
70
|
+
apiKey?: string;
|
|
71
|
+
/** Test seam / custom transport. Defaults to the global `fetch`. */
|
|
72
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
73
|
+
now?: () => number;
|
|
74
|
+
logger?: { warn: (message: string) => void };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Live OpenRouter prices keyed by model id ($/million tokens). Cached for an
|
|
79
|
+
* hour. Never throws: on any failure it returns the last cached map, or an
|
|
80
|
+
* empty map (so the caller falls back to the catalog's baseline prices).
|
|
81
|
+
*/
|
|
82
|
+
export async function fetchOpenRouterPrices(
|
|
83
|
+
opts: FetchOpenRouterPricesOptions = {},
|
|
84
|
+
): Promise<ReadonlyMap<string, ModelPrice>> {
|
|
85
|
+
const now = opts.now ?? Date.now;
|
|
86
|
+
if (cache && now() - cache.fetchedAt < OPENROUTER_PRICE_CACHE_TTL_MS) {
|
|
87
|
+
return cache.prices;
|
|
88
|
+
}
|
|
89
|
+
// The global `fetch` must be called as a function of the global scope on
|
|
90
|
+
// workerd ("Illegal invocation" when handed around as a bare reference).
|
|
91
|
+
const fetchImpl =
|
|
92
|
+
opts.fetch ?? ((input: string, init?: RequestInit) => fetch(input, init));
|
|
93
|
+
const logger = opts.logger ?? console;
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetchImpl(OPENROUTER_MODELS_URL, {
|
|
96
|
+
headers: opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {},
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`OpenRouter /models returned HTTP ${response.status}`);
|
|
100
|
+
}
|
|
101
|
+
const parsed = openRouterModelsSchema.safeParse(await response.json());
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
throw new Error(`Unexpected /models payload: ${parsed.error.message}`);
|
|
104
|
+
}
|
|
105
|
+
const prices = parsePrices(parsed.data.data);
|
|
106
|
+
if (prices.size === 0) {
|
|
107
|
+
throw new Error('OpenRouter /models returned no usable pricing');
|
|
108
|
+
}
|
|
109
|
+
cache = { fetchedAt: now(), prices };
|
|
110
|
+
return prices;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logger.warn(
|
|
113
|
+
`[models] live OpenRouter pricing unavailable — serving baseline prices: ${
|
|
114
|
+
error instanceof Error ? error.message : String(error)
|
|
115
|
+
}`,
|
|
116
|
+
);
|
|
117
|
+
return cache?.prices ?? new Map<string, ModelPrice>();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Test seam: clear the in-memory price cache between cases. */
|
|
122
|
+
export function resetOpenRouterPriceCache(): void {
|
|
123
|
+
cache = null;
|
|
124
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { OraclePlugin, PluginEnv } from '../plugin-api/oracle-plugin';
|
|
2
|
+
import type { Logger } from '../plugin-api/types';
|
|
3
|
+
import { NOOP_LOGGER } from './utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-feature toggle:
|
|
7
|
+
* - `true` — explicit opt-in. If the plugin has an `autoDetect` and it
|
|
8
|
+
* fails, boot fails.
|
|
9
|
+
* - `false` — explicit opt-out.
|
|
10
|
+
* - `'auto'` — include only if the plugin's `autoDetect` returns true (or
|
|
11
|
+
* the plugin has no `autoDetect`, in which case it loads).
|
|
12
|
+
*/
|
|
13
|
+
export type FeatureToggle = boolean | 'auto';
|
|
14
|
+
|
|
15
|
+
export type ExclusionCause =
|
|
16
|
+
| 'feature_false'
|
|
17
|
+
| 'auto_detect_missing'
|
|
18
|
+
| 'cascaded';
|
|
19
|
+
|
|
20
|
+
export interface ExcludedPlugin {
|
|
21
|
+
plugin: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
cause: ExclusionCause;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SoftDepGap {
|
|
27
|
+
plugin: string;
|
|
28
|
+
missing: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolvePluginsInput {
|
|
32
|
+
features?: Partial<Record<string, FeatureToggle>>;
|
|
33
|
+
/** Plugins subject to feature toggles + `autoDetect`. */
|
|
34
|
+
bundled: OraclePlugin[];
|
|
35
|
+
/** Plugins that always load (no toggle, no auto-detect). */
|
|
36
|
+
userPlugins?: OraclePlugin[];
|
|
37
|
+
/**
|
|
38
|
+
* The Worker `env` bindings object handed to each plugin's `autoDetect`.
|
|
39
|
+
* Required on Workers — there is no `process.env` to fall back to.
|
|
40
|
+
*/
|
|
41
|
+
env: PluginEnv;
|
|
42
|
+
logger?: Logger;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvePluginsResult {
|
|
46
|
+
loaded: OraclePlugin[];
|
|
47
|
+
excluded: ExcludedPlugin[];
|
|
48
|
+
softDepGaps: SoftDepGap[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the final plugin list for boot.
|
|
53
|
+
*
|
|
54
|
+
* 1. Apply feature toggles + each plugin's `autoDetect` to the bundled list.
|
|
55
|
+
* 2. Combine with always-loaded user plugins.
|
|
56
|
+
* 3. Cascade hard-dep removals (transitive — fixed-point).
|
|
57
|
+
* 4. Topologically sort by `dependsOn`.
|
|
58
|
+
* 5. Log soft-dep gaps.
|
|
59
|
+
*/
|
|
60
|
+
export function resolvePlugins(
|
|
61
|
+
input: ResolvePluginsInput,
|
|
62
|
+
): ResolvePluginsResult {
|
|
63
|
+
const env = input.env;
|
|
64
|
+
const logger = input.logger ?? NOOP_LOGGER;
|
|
65
|
+
const features = input.features ?? {};
|
|
66
|
+
|
|
67
|
+
const excluded: ExcludedPlugin[] = [];
|
|
68
|
+
const survivors: OraclePlugin[] = [];
|
|
69
|
+
|
|
70
|
+
for (const plugin of input.bundled) {
|
|
71
|
+
const toggle = features[plugin.name];
|
|
72
|
+
|
|
73
|
+
if (toggle === false) {
|
|
74
|
+
excluded.push({
|
|
75
|
+
plugin: plugin.name,
|
|
76
|
+
reason: 'feature flag set to false',
|
|
77
|
+
cause: 'feature_false',
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const detectOk = plugin.autoDetect ? plugin.autoDetect(env) : true;
|
|
83
|
+
const hint = plugin.autoDetectHint ?? 'auto-detect predicate';
|
|
84
|
+
|
|
85
|
+
if (toggle === true && !detectOk) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`boot.plugin.env_missing: plugin '${plugin.name}' enabled via features but precondition failed (${hint}). ` +
|
|
88
|
+
`Set the required env or disable: features: { ${plugin.name}: false }`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!detectOk) {
|
|
93
|
+
excluded.push({
|
|
94
|
+
plugin: plugin.name,
|
|
95
|
+
reason: `auto-detect precondition not met (${hint})`,
|
|
96
|
+
cause: 'auto_detect_missing',
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
survivors.push(plugin);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const allLoaded = [...survivors, ...(input.userPlugins ?? [])];
|
|
105
|
+
|
|
106
|
+
// Transitive cascade — fixed-point. A plugin cascades off if any of its
|
|
107
|
+
// hard deps is excluded for any reason (explicit false, auto-detect, or
|
|
108
|
+
// a previous cascade).
|
|
109
|
+
const kept = new Map(allLoaded.map((p) => [p.name, p]));
|
|
110
|
+
const excludedNames = new Set(excluded.map((e) => e.plugin));
|
|
111
|
+
|
|
112
|
+
let changed = true;
|
|
113
|
+
while (changed) {
|
|
114
|
+
changed = false;
|
|
115
|
+
for (const plugin of [...kept.values()]) {
|
|
116
|
+
for (const dep of plugin.dependsOn ?? []) {
|
|
117
|
+
if (kept.has(dep)) continue;
|
|
118
|
+
if (!excludedNames.has(dep)) continue;
|
|
119
|
+
const reason = `cascaded off via ${dep}`;
|
|
120
|
+
excluded.push({ plugin: plugin.name, reason, cause: 'cascaded' });
|
|
121
|
+
excludedNames.add(plugin.name);
|
|
122
|
+
kept.delete(plugin.name);
|
|
123
|
+
logger.warn(
|
|
124
|
+
`[boot] plugin '${plugin.name}' ${reason} (event: boot.plugin.cascaded_off)`,
|
|
125
|
+
);
|
|
126
|
+
changed = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ordered = topoSort([...kept.values()]);
|
|
133
|
+
const finalNames = new Set(ordered.map((p) => p.name));
|
|
134
|
+
|
|
135
|
+
const softDepGaps: SoftDepGap[] = [];
|
|
136
|
+
for (const plugin of ordered) {
|
|
137
|
+
for (const soft of plugin.softDependsOn ?? []) {
|
|
138
|
+
if (finalNames.has(soft)) continue;
|
|
139
|
+
softDepGaps.push({ plugin: plugin.name, missing: soft });
|
|
140
|
+
logger.log(
|
|
141
|
+
`[boot] plugin '${plugin.name}' soft-depends on '${soft}', which is not loaded (event: boot.plugin.soft_dep_missing)`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { loaded: ordered, excluded, softDepGaps };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Topologically sort plugins by `dependsOn`. Throws on unmet hard deps
|
|
151
|
+
* (`boot.plugin.dep_missing`) or cycles (`boot.plugin.cycle`, with the
|
|
152
|
+
* cycle path included in the message).
|
|
153
|
+
*/
|
|
154
|
+
export function topoSort(plugins: OraclePlugin[]): OraclePlugin[] {
|
|
155
|
+
const byName = new Map(plugins.map((p) => [p.name, p]));
|
|
156
|
+
const visited = new Set<string>();
|
|
157
|
+
const visiting = new Set<string>();
|
|
158
|
+
const ordered: OraclePlugin[] = [];
|
|
159
|
+
|
|
160
|
+
const visit = (
|
|
161
|
+
name: string,
|
|
162
|
+
requiredBy: string | null,
|
|
163
|
+
path: string[],
|
|
164
|
+
): void => {
|
|
165
|
+
if (visited.has(name)) return;
|
|
166
|
+
if (visiting.has(name)) {
|
|
167
|
+
const cycle = [...path.slice(path.indexOf(name)), name].join(' -> ');
|
|
168
|
+
throw new Error(
|
|
169
|
+
`boot.plugin.cycle: dependency cycle detected: ${cycle}. ` +
|
|
170
|
+
`Break the cycle by removing one of the dependsOn links.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const plugin = byName.get(name);
|
|
174
|
+
if (!plugin) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`boot.plugin.dep_missing: plugin '${requiredBy}' requires '${name}', which is not loaded. ` +
|
|
177
|
+
`Add '${name}' to features, or remove '${requiredBy}'.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
visiting.add(name);
|
|
182
|
+
for (const dep of plugin.dependsOn ?? []) {
|
|
183
|
+
visit(dep, name, [...path, name]);
|
|
184
|
+
}
|
|
185
|
+
visiting.delete(name);
|
|
186
|
+
visited.add(name);
|
|
187
|
+
ordered.push(plugin);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
for (const p of plugins) visit(p.name, null, []);
|
|
191
|
+
return ordered;
|
|
192
|
+
}
|