@ixo/oracle-runtime-workers 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/package.json +14 -9
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +198 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +523 -111
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AmbiguousDecisionProviderError,
|
|
3
|
+
CAPABILITY_ROUTER_MODES,
|
|
4
|
+
CAPABILITY_ROUTE_DECISION_NAME,
|
|
5
|
+
DecisionProviderUnavailableError,
|
|
6
|
+
capabilityRouteDecision,
|
|
7
|
+
decideCapabilityRoute,
|
|
8
|
+
toRoutableCapabilities,
|
|
9
|
+
type CapabilityRouterMode,
|
|
10
|
+
type DecisionEvaluator,
|
|
11
|
+
type DecisionTraceOptions,
|
|
12
|
+
type RoutableCapability,
|
|
13
|
+
} from '@ixo/common/ai/decisions';
|
|
14
|
+
import type { Logger } from '../plugin-api/types';
|
|
15
|
+
import { unmetRequirements } from './manifest';
|
|
16
|
+
import type { RegisteredManifest } from './registries';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Predicts, before the first model call of a turn, which on-demand plugin the
|
|
20
|
+
* user's message needs and preloads that plugin's tools for the turn, so the
|
|
21
|
+
* model does not spend a round trip on `load_capability`.
|
|
22
|
+
*
|
|
23
|
+
* The preload is one-turn state only: it is admitted by the capability gate
|
|
24
|
+
* and visible as loaded to tool handlers, but never written to the graph's
|
|
25
|
+
* `loadedPlugins` channel. The router fails open — any evaluation failure
|
|
26
|
+
* preloads nothing and the turn proceeds exactly as without a router.
|
|
27
|
+
*/
|
|
28
|
+
export interface CapabilityRouterOptions {
|
|
29
|
+
evaluator: DecisionEvaluator;
|
|
30
|
+
logger: Logger;
|
|
31
|
+
/**
|
|
32
|
+
* Keeps a shadow evaluation alive after the turn has moved on
|
|
33
|
+
* (`ctx.waitUntil` on Workers). Without it the promise is merely detached.
|
|
34
|
+
*/
|
|
35
|
+
background?: (work: Promise<unknown>) => void;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CapabilityRouteTurn {
|
|
40
|
+
mode: CapabilityRouterMode;
|
|
41
|
+
/** Every registered manifest; the router picks the routable candidates. */
|
|
42
|
+
manifests: readonly RegisteredManifest[];
|
|
43
|
+
/** Plugins the thread has already loaded (the checkpointed channel). */
|
|
44
|
+
loaded: ReadonlySet<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Plugins the user's delegation leaves with nothing to use (all of their
|
|
47
|
+
* tools admin-plane and not granted): never offered to the router. A
|
|
48
|
+
* function is called only when the router actually evaluates (not when
|
|
49
|
+
* it is off, or the message is empty), so the host does not compute the
|
|
50
|
+
* set for nothing.
|
|
51
|
+
*/
|
|
52
|
+
hidden?: ReadonlySet<string> | (() => Promise<ReadonlySet<string>>);
|
|
53
|
+
/**
|
|
54
|
+
* The user's delegation, as `ctx.ucan.hasCapability`: a plugin whose
|
|
55
|
+
* `manifest.requires` it does not grant is never offered (the gate would
|
|
56
|
+
* refuse every one of its tools). Omitted, `requires` is not checked.
|
|
57
|
+
*/
|
|
58
|
+
hasCapability?: (resource: string, action: string) => boolean;
|
|
59
|
+
/** The user's message text for this turn. */
|
|
60
|
+
text: string;
|
|
61
|
+
requestId: string;
|
|
62
|
+
/** The turn's abort signal; only an awaited (`on`) evaluation is bound to it. */
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
/**
|
|
65
|
+
* The turn's tracer and trace metadata. The router runs before the graph,
|
|
66
|
+
* outside any LangChain run, so without these its Decision is not traced.
|
|
67
|
+
*/
|
|
68
|
+
trace?: DecisionTraceOptions;
|
|
69
|
+
/**
|
|
70
|
+
* Shadow mode only: receives what the router would have preloaded once its
|
|
71
|
+
* evaluation completes, so the host can compare it with what the turn
|
|
72
|
+
* actually loaded.
|
|
73
|
+
*/
|
|
74
|
+
onShadowVerdict?: (wouldPreload: readonly string[]) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type CapabilityRouter = (
|
|
78
|
+
turn: CapabilityRouteTurn,
|
|
79
|
+
) => Promise<ReadonlySet<string>>;
|
|
80
|
+
|
|
81
|
+
const NOTHING: ReadonlySet<string> = new Set<string>();
|
|
82
|
+
|
|
83
|
+
/** Narrows an env value to a router mode; anything else is `off`. */
|
|
84
|
+
export function capabilityRouterMode(value: unknown): CapabilityRouterMode {
|
|
85
|
+
return CAPABILITY_ROUTER_MODES.find((mode) => mode === value) ?? 'off';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The manifests the router may choose from: effectively on-demand plugins the
|
|
90
|
+
* thread has not loaded yet. `always` plugins are already bound and `silent`
|
|
91
|
+
* ones are never surfaced, so predicting them is pointless; a `hidden` one
|
|
92
|
+
* (no tool the user's delegation reaches), or one whose `requires` the
|
|
93
|
+
* delegation (`hasCapability`) does not grant, is not even named to the
|
|
94
|
+
* router.
|
|
95
|
+
*/
|
|
96
|
+
export function routableCandidates(
|
|
97
|
+
manifests: readonly RegisteredManifest[],
|
|
98
|
+
loaded: ReadonlySet<string>,
|
|
99
|
+
hidden: ReadonlySet<string> = NOTHING,
|
|
100
|
+
hasCapability?: (resource: string, action: string) => boolean,
|
|
101
|
+
): RoutableCapability[] {
|
|
102
|
+
return toRoutableCapabilities(
|
|
103
|
+
manifests
|
|
104
|
+
.filter(
|
|
105
|
+
({ pluginName, manifest }) =>
|
|
106
|
+
(manifest.visibility ?? 'on-demand') === 'on-demand' &&
|
|
107
|
+
!loaded.has(pluginName) &&
|
|
108
|
+
!hidden.has(pluginName) &&
|
|
109
|
+
(hasCapability === undefined ||
|
|
110
|
+
unmetRequirements(manifest, hasCapability).length === 0),
|
|
111
|
+
)
|
|
112
|
+
.map(({ pluginName, manifest }) => ({
|
|
113
|
+
name: pluginName,
|
|
114
|
+
title: manifest.title,
|
|
115
|
+
summary: manifest.summary,
|
|
116
|
+
})),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* After-turn comparison for shadow mode: which plugins the turn loaded on its
|
|
122
|
+
* own, and whether that matches what the router would have preloaded.
|
|
123
|
+
*/
|
|
124
|
+
export function shadowAgreement(args: {
|
|
125
|
+
priorLoaded: ReadonlySet<string>;
|
|
126
|
+
loadedAfter: ReadonlySet<string>;
|
|
127
|
+
wouldPreload: readonly string[];
|
|
128
|
+
}): { loadedDuringTurn: string[]; agree: boolean } {
|
|
129
|
+
const loadedDuringTurn = Array.from(args.loadedAfter).filter(
|
|
130
|
+
(name) => !args.priorLoaded.has(name),
|
|
131
|
+
);
|
|
132
|
+
const predicted = new Set(args.wouldPreload);
|
|
133
|
+
const agree =
|
|
134
|
+
predicted.size === loadedDuringTurn.length &&
|
|
135
|
+
loadedDuringTurn.every((name) => predicted.has(name));
|
|
136
|
+
return { loadedDuringTurn, agree };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function errorName(error: unknown): string {
|
|
140
|
+
return error instanceof Error ? error.name : typeof error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function list(names: readonly string[]): string {
|
|
144
|
+
return `[${names.join(', ')}]`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createCapabilityRouter(
|
|
148
|
+
options: CapabilityRouterOptions,
|
|
149
|
+
): CapabilityRouter {
|
|
150
|
+
const { evaluator, logger } = options;
|
|
151
|
+
const now = options.now ?? (() => Date.now());
|
|
152
|
+
const background =
|
|
153
|
+
options.background ??
|
|
154
|
+
((work: Promise<unknown>) => {
|
|
155
|
+
void work;
|
|
156
|
+
});
|
|
157
|
+
// A missing Decision provider is a deployment fact, not a per-turn event:
|
|
158
|
+
// said once per object, then the router stays quiet.
|
|
159
|
+
let unavailableWarned = false;
|
|
160
|
+
|
|
161
|
+
// An ambiguous multi-provider configuration is a subclass: equally a
|
|
162
|
+
// deployment fact, but fixed by routing rather than by adding a provider.
|
|
163
|
+
const unavailable = (
|
|
164
|
+
error: unknown,
|
|
165
|
+
): error is DecisionProviderUnavailableError =>
|
|
166
|
+
error instanceof DecisionProviderUnavailableError;
|
|
167
|
+
|
|
168
|
+
const warnUnavailable = (
|
|
169
|
+
requestId: string,
|
|
170
|
+
mode: CapabilityRouterMode,
|
|
171
|
+
error: DecisionProviderUnavailableError,
|
|
172
|
+
) => {
|
|
173
|
+
if (unavailableWarned) return;
|
|
174
|
+
unavailableWarned = true;
|
|
175
|
+
const hint =
|
|
176
|
+
error instanceof AmbiguousDecisionProviderError
|
|
177
|
+
? `route ${CAPABILITY_ROUTE_DECISION_NAME} or set decisionProviderPolicy.defaultProviderId`
|
|
178
|
+
: 'set DECISION_PROVIDER or CAPABILITY_ROUTER=off';
|
|
179
|
+
logger.warn(
|
|
180
|
+
`[capability-router] request=${requestId} mode=${mode} status=fallback reason=${error.name} (${hint})`,
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return async (turn) => {
|
|
185
|
+
if (turn.mode === 'off') return NOTHING;
|
|
186
|
+
const text = turn.text.trim();
|
|
187
|
+
if (!text) return NOTHING;
|
|
188
|
+
let hidden: ReadonlySet<string> | undefined;
|
|
189
|
+
try {
|
|
190
|
+
hidden =
|
|
191
|
+
typeof turn.hidden === 'function' ? await turn.hidden() : turn.hidden;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Fail open like any other router failure: no preload this turn.
|
|
194
|
+
logger.warn(
|
|
195
|
+
`[capability-router] request=${turn.requestId} mode=${turn.mode} status=fallback reason=${errorName(error)} (hidden plugins unavailable)`,
|
|
196
|
+
);
|
|
197
|
+
return NOTHING;
|
|
198
|
+
}
|
|
199
|
+
const capabilities = routableCandidates(
|
|
200
|
+
turn.manifests,
|
|
201
|
+
turn.loaded,
|
|
202
|
+
hidden,
|
|
203
|
+
turn.hasCapability,
|
|
204
|
+
);
|
|
205
|
+
if (capabilities.length === 0) return NOTHING;
|
|
206
|
+
|
|
207
|
+
// The turn is prepared from a checkpoint read without its messages, so
|
|
208
|
+
// the router sees the current message only; `recentTurns` stays empty.
|
|
209
|
+
const input = { text, recentTurns: [], capabilities };
|
|
210
|
+
const { requestId } = turn;
|
|
211
|
+
|
|
212
|
+
if (turn.mode === 'shadow') {
|
|
213
|
+
const started = now();
|
|
214
|
+
background(
|
|
215
|
+
evaluator
|
|
216
|
+
.evaluate(capabilityRouteDecision, input, turn.trace)
|
|
217
|
+
.then((evaluation) => {
|
|
218
|
+
const verdict = decideCapabilityRoute(evaluation, capabilities);
|
|
219
|
+
logger.log(
|
|
220
|
+
`[capability-router-shadow] request=${requestId} status=ok ` +
|
|
221
|
+
`needsCapability=${verdict.needsCapability.toFixed(2)} ` +
|
|
222
|
+
`capability=${verdict.capability ?? '-'} ` +
|
|
223
|
+
`capabilityConfidence=${verdict.capabilityConfidence?.toFixed(2) ?? '-'} ` +
|
|
224
|
+
`wouldPreload=${list(verdict.preload)} reason=${verdict.reason} ` +
|
|
225
|
+
`latencyMs=${now() - started} provider=${evaluation.provider} model=${evaluation.model}`,
|
|
226
|
+
);
|
|
227
|
+
turn.onShadowVerdict?.(verdict.preload);
|
|
228
|
+
})
|
|
229
|
+
.catch((error: unknown) => {
|
|
230
|
+
if (unavailable(error)) {
|
|
231
|
+
warnUnavailable(requestId, 'shadow', error);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
logger.log(
|
|
235
|
+
`[capability-router-shadow] request=${requestId} status=failed errorType=${errorName(error)} latencyMs=${now() - started}`,
|
|
236
|
+
);
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
return NOTHING;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const evaluation = await evaluator.evaluate(
|
|
244
|
+
capabilityRouteDecision,
|
|
245
|
+
input,
|
|
246
|
+
{
|
|
247
|
+
...turn.trace,
|
|
248
|
+
...(turn.signal && { signal: turn.signal }),
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
const verdict = decideCapabilityRoute(evaluation, capabilities);
|
|
252
|
+
if (verdict.preload.length === 0) {
|
|
253
|
+
logger.debug?.(
|
|
254
|
+
`[capability-router] request=${requestId} mode=on status=none reason=${verdict.reason} candidates=${capabilities.length}`,
|
|
255
|
+
);
|
|
256
|
+
return NOTHING;
|
|
257
|
+
}
|
|
258
|
+
logger.log(
|
|
259
|
+
`[capability-router] request=${requestId} preloaded=${list(verdict.preload)} candidates=${capabilities.length}`,
|
|
260
|
+
);
|
|
261
|
+
return new Set(verdict.preload);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (unavailable(error)) {
|
|
264
|
+
warnUnavailable(requestId, 'on', error);
|
|
265
|
+
} else {
|
|
266
|
+
logger.warn(
|
|
267
|
+
`[capability-router] request=${requestId} mode=on status=fallback reason=${errorName(error)}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return NOTHING;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* (`context-window.ts`) so that they scale with the model:
|
|
4
4
|
*
|
|
5
5
|
* - `summarizeAtTokens` — the history is condensed once it is this large
|
|
6
|
-
* (default half the window; Hermes fires at 50%)
|
|
6
|
+
* (default half the window; Hermes fires at 50%),
|
|
7
|
+
* never above what the summarizer may read;
|
|
7
8
|
* - `pruneAtTokens` — above this, old tool results are demoted to
|
|
8
9
|
* one-liners in the request (no model call, no
|
|
9
10
|
* state change) before the model sees them;
|
|
@@ -64,8 +65,14 @@ export interface ContextBudget {
|
|
|
64
65
|
resultCapChars: number;
|
|
65
66
|
requestCapTokens: number;
|
|
66
67
|
outputReserveTokens: number;
|
|
67
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* The summarizer's own input limit (what it may read to write the summary):
|
|
70
|
+
* the smaller of what the main model's request cap and the summarizing
|
|
71
|
+
* model's own request cap leave, less a margin for the summary prompt.
|
|
72
|
+
*/
|
|
68
73
|
summaryInputTokens: number;
|
|
74
|
+
/** The summarizing model's window, when the host resolved it. */
|
|
75
|
+
summaryWindowTokens?: number;
|
|
69
76
|
summarizeTriggerMessages?: number;
|
|
70
77
|
keepMessages: number;
|
|
71
78
|
}
|
|
@@ -142,12 +149,14 @@ export function contextKnobs(
|
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
151
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
/** Tokens the summary prompt and its framing need on top of the history it reads. */
|
|
153
|
+
const SUMMARY_PROMPT_MARGIN_TOKENS = 4_000;
|
|
154
|
+
|
|
155
|
+
/** What a request to a model with `window` may hold, the reply reserve left out. */
|
|
156
|
+
function requestCapFor(
|
|
157
|
+
window: number,
|
|
158
|
+
knobs: ContextKnobs,
|
|
159
|
+
): { requestCapTokens: number; outputReserveTokens: number } {
|
|
151
160
|
// The reply reserve can never eat a small window: cap it at a quarter.
|
|
152
161
|
const outputReserveTokens = Math.min(
|
|
153
162
|
knobs.outputReserveTokens,
|
|
@@ -157,9 +166,42 @@ export function contextBudgetFor(
|
|
|
157
166
|
1_000,
|
|
158
167
|
Math.floor(window * knobs.requestFraction) - outputReserveTokens,
|
|
159
168
|
);
|
|
169
|
+
return { requestCapTokens, outputReserveTokens };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The budget for a turn on `resolution.model`. `summarizer` is the window of
|
|
174
|
+
* the model that writes the summary (the `routing` role), resolved the same
|
|
175
|
+
* way: the history it is handed is bounded by its own window too, not only
|
|
176
|
+
* by the main model's. Omitted, the main model's window alone bounds it.
|
|
177
|
+
*/
|
|
178
|
+
export function contextBudgetFor(
|
|
179
|
+
resolution: ContextWindowResolution,
|
|
180
|
+
knobs: ContextKnobs = DEFAULT_CONTEXT_KNOBS,
|
|
181
|
+
summarizer?: ContextWindowResolution,
|
|
182
|
+
): ContextBudget {
|
|
183
|
+
const window = resolution.tokens;
|
|
184
|
+
const { requestCapTokens, outputReserveTokens } = requestCapFor(
|
|
185
|
+
window,
|
|
186
|
+
knobs,
|
|
187
|
+
);
|
|
188
|
+
const summaryRequestCap = summarizer
|
|
189
|
+
? Math.min(
|
|
190
|
+
requestCapTokens,
|
|
191
|
+
requestCapFor(summarizer.tokens, knobs).requestCapTokens,
|
|
192
|
+
)
|
|
193
|
+
: requestCapTokens;
|
|
194
|
+
const summaryInputTokens = Math.max(
|
|
195
|
+
1_000,
|
|
196
|
+
summaryRequestCap - SUMMARY_PROMPT_MARGIN_TOKENS,
|
|
197
|
+
);
|
|
198
|
+
// The summarizer keeps only the newest `summaryInputTokens` of what it is
|
|
199
|
+
// asked to condense and drops the rest unread — the earlier summary first.
|
|
200
|
+
// Summarizing starts no later than that, so nothing is lost unsummarized.
|
|
160
201
|
const summarizeAtTokens = Math.min(
|
|
161
202
|
Math.floor(window * knobs.summarizeFraction),
|
|
162
203
|
requestCapTokens,
|
|
204
|
+
summaryInputTokens,
|
|
163
205
|
);
|
|
164
206
|
const pruneAtTokens = Math.min(
|
|
165
207
|
Math.floor(window * knobs.pruneFraction),
|
|
@@ -177,7 +219,8 @@ export function contextBudgetFor(
|
|
|
177
219
|
),
|
|
178
220
|
requestCapTokens,
|
|
179
221
|
outputReserveTokens,
|
|
180
|
-
summaryInputTokens
|
|
222
|
+
summaryInputTokens,
|
|
223
|
+
...(summarizer ? { summaryWindowTokens: summarizer.tokens } : {}),
|
|
181
224
|
...(knobs.summarizeTriggerMessages !== undefined
|
|
182
225
|
? { summarizeTriggerMessages: knobs.summarizeTriggerMessages }
|
|
183
226
|
: {}),
|
|
@@ -196,6 +239,50 @@ export function estimateContentTokens(content: unknown): number {
|
|
|
196
239
|
}
|
|
197
240
|
}
|
|
198
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Per-message memo of the chars/4 estimates: every model step of a turn
|
|
244
|
+
* re-estimates the whole history (the context guard, the turn budget, the
|
|
245
|
+
* summarizer's trigger), and the history is the same message objects from
|
|
246
|
+
* step to step. An entry is reused only while the message still holds the
|
|
247
|
+
* same `content` / `tool_calls` values it was computed from.
|
|
248
|
+
*/
|
|
249
|
+
interface MessageEstimate {
|
|
250
|
+
content: unknown;
|
|
251
|
+
contentTokens: number;
|
|
252
|
+
toolCalls?: { value: unknown; tokens: number };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const messageEstimates = new WeakMap<object, MessageEstimate>();
|
|
256
|
+
|
|
257
|
+
function estimateOf(message: { content: unknown }): MessageEstimate {
|
|
258
|
+
const cached = messageEstimates.get(message);
|
|
259
|
+
if (cached && cached.content === message.content) return cached;
|
|
260
|
+
const fresh: MessageEstimate = {
|
|
261
|
+
content: message.content,
|
|
262
|
+
contentTokens: estimateContentTokens(message.content),
|
|
263
|
+
};
|
|
264
|
+
messageEstimates.set(message, fresh);
|
|
265
|
+
return fresh;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** `estimateContentTokens(message.content)`, memoised per message. */
|
|
269
|
+
export function messageContentTokens(message: { content: unknown }): number {
|
|
270
|
+
return estimateOf(message).contentTokens;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** `estimateContentTokens(toolCalls)` for `message`'s tool calls, memoised per message. */
|
|
274
|
+
export function messageToolCallTokens(
|
|
275
|
+
message: { content: unknown },
|
|
276
|
+
toolCalls: unknown,
|
|
277
|
+
): number {
|
|
278
|
+
const entry = estimateOf(message);
|
|
279
|
+
if (entry.toolCalls && entry.toolCalls.value === toolCalls)
|
|
280
|
+
return entry.toolCalls.tokens;
|
|
281
|
+
const tokens = estimateContentTokens(toolCalls);
|
|
282
|
+
entry.toolCalls = { value: toolCalls, tokens };
|
|
283
|
+
return tokens;
|
|
284
|
+
}
|
|
285
|
+
|
|
199
286
|
export function describeBudget(b: ContextBudget): string {
|
|
200
|
-
return `model=${b.model} window=${b.windowTokens} (${b.origin}) summarizeAt=${b.summarizeAtTokens} pruneAt=${b.pruneAtTokens} resultCap=${b.resultCapChars}c requestCap=${b.requestCapTokens} reserve=${b.outputReserveTokens}`;
|
|
287
|
+
return `model=${b.model} window=${b.windowTokens} (${b.origin}) summarizeAt=${b.summarizeAtTokens} pruneAt=${b.pruneAtTokens} resultCap=${b.resultCapChars}c requestCap=${b.requestCapTokens} reserve=${b.outputReserveTokens} summaryInput=${b.summaryInputTokens}${b.summaryWindowTokens !== undefined ? ` (summarizer window ${b.summaryWindowTokens})` : ''}`;
|
|
201
288
|
}
|
package/src/core/env.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
capabilityRouterEnvShape,
|
|
3
|
+
decisionProviderEnvShape,
|
|
4
|
+
} from '@ixo/common/ai/decisions';
|
|
1
5
|
import { z } from 'zod';
|
|
2
6
|
import type { OraclePlugin } from '../plugin-api/oracle-plugin';
|
|
3
7
|
import type { Logger } from '../plugin-api/types';
|
|
4
8
|
import { NOOP_LOGGER } from './utils';
|
|
9
|
+
import { FEEDBACK_ENV_PREFIX } from '../feedback/contract';
|
|
10
|
+
import {
|
|
11
|
+
isSecureUrl,
|
|
12
|
+
MAX_ARTIFACT_SWEEP_INTERVAL_HOURS,
|
|
13
|
+
MAX_ARTIFACT_TTL_DAYS,
|
|
14
|
+
} from '../artifacts/config';
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* Tier-0 (core) environment for the Workers runtime. Mirrors
|
|
@@ -29,6 +39,11 @@ import { NOOP_LOGGER } from './utils';
|
|
|
29
39
|
*/
|
|
30
40
|
export const TURN_RECURSION_LIMIT_DEFAULT = 600;
|
|
31
41
|
|
|
42
|
+
/** An https URL, or http on localhost / 127.0.0.1 (local harness). */
|
|
43
|
+
const secureUrlSchema = z.string().refine((raw) => isSecureUrl(raw), {
|
|
44
|
+
message: 'must be an https URL (http only on localhost)',
|
|
45
|
+
});
|
|
46
|
+
|
|
32
47
|
export const baseEnvSchema = z.object({
|
|
33
48
|
// --- identity -----------------------------------------------------------
|
|
34
49
|
ORACLE_NAME: z.string().min(1),
|
|
@@ -50,6 +65,13 @@ export const baseEnvSchema = z.object({
|
|
|
50
65
|
* declares. Default 15 minutes.
|
|
51
66
|
*/
|
|
52
67
|
UCAN_AUTH_MAX_TTL_SECONDS: z.coerce.number().int().positive().default(900),
|
|
68
|
+
/**
|
|
69
|
+
* `true` lets a bare `x-ucan-delegation` (no invocation) authenticate a
|
|
70
|
+
* request — the legacy fallback, for clients that do not send a UCAN
|
|
71
|
+
* invocation yet. Off by default: a delegation travels on as proof in
|
|
72
|
+
* downstream invocations, so it must not also be a login credential.
|
|
73
|
+
*/
|
|
74
|
+
UCAN_ALLOW_BARE_DELEGATION_AUTH: z.enum(['true', 'false']).optional(),
|
|
53
75
|
|
|
54
76
|
// --- matrix -------------------------------------------------------------
|
|
55
77
|
MATRIX_BASE_URL: z.string().min(1),
|
|
@@ -114,6 +136,45 @@ export const baseEnvSchema = z.object({
|
|
|
114
136
|
.int()
|
|
115
137
|
.min(1)
|
|
116
138
|
.default(TURN_RECURSION_LIMIT_DEFAULT),
|
|
139
|
+
/**
|
|
140
|
+
* The turn budget (docs/plans/workers-harness-hardening.md): cumulative
|
|
141
|
+
* model tokens across the main agent, sub-agents and helper models; tool
|
|
142
|
+
* attempts; and the wall-clock deadline. Exhaustion ends the turn and keeps
|
|
143
|
+
* the work done so far. Parsed by `turnLimitsFromEnv`.
|
|
144
|
+
*/
|
|
145
|
+
TURN_MAX_TOKENS: z.coerce.number().int().positive().default(500_000),
|
|
146
|
+
TURN_MAX_TOOL_CALLS: z.coerce.number().int().positive().default(120),
|
|
147
|
+
TURN_TIMEOUT_MS: z.coerce.number().int().positive().default(600_000),
|
|
148
|
+
/**
|
|
149
|
+
* The repetition guard's per-turn caps on identical successful calls
|
|
150
|
+
* (same tool, same arguments): a write, and a read or `repeatable` tool.
|
|
151
|
+
* Parsed by `repetitionCapsFromEnv`.
|
|
152
|
+
*/
|
|
153
|
+
TURN_MAX_IDENTICAL_WRITES: z.coerce.number().int().positive().default(1),
|
|
154
|
+
TURN_MAX_IDENTICAL_READS: z.coerce.number().int().positive().default(5),
|
|
155
|
+
/**
|
|
156
|
+
* Time limit per plugin `getRequestAdmission` handler (ms). A handler that
|
|
157
|
+
* has not answered by then counts as `pass` and the turn goes on to the
|
|
158
|
+
* next handler or the agent. See docs/architecture/request-admission.md.
|
|
159
|
+
*/
|
|
160
|
+
REQUEST_ADMISSION_TIMEOUT_MS: z.coerce
|
|
161
|
+
.number()
|
|
162
|
+
.int()
|
|
163
|
+
.positive()
|
|
164
|
+
.default(2_000),
|
|
165
|
+
|
|
166
|
+
// --- decisions ----------------------------------------------------------
|
|
167
|
+
// Bounded semantic Decision provider (`DECISION_PROVIDER`, `DECISION_MODEL`,
|
|
168
|
+
// Cloudflare credentials), shared with the Node runtime and optional so
|
|
169
|
+
// oracles without Decisions keep their behaviour. `openrouter-jev` reuses
|
|
170
|
+
// `OPEN_ROUTER_API_KEY` (required above). On Workers `cloudflare-jev`
|
|
171
|
+
// prefers the `AI` binding over the account credentials, which are only
|
|
172
|
+
// needed when the binding is not declared.
|
|
173
|
+
...decisionProviderEnvShape,
|
|
174
|
+
// `CAPABILITY_ROUTER` (`off` | `shadow` | `on`, default `off`): the first
|
|
175
|
+
// runtime consumer of Decisions. It predicts the on-demand plugin a message
|
|
176
|
+
// needs and preloads it for the turn; it needs a `DECISION_PROVIDER`.
|
|
177
|
+
...capabilityRouterEnvShape,
|
|
117
178
|
|
|
118
179
|
// --- durable runs (docs/plans/durable-runs.md) ---------------------------
|
|
119
180
|
// Parsed by `runDurabilityConfig` (src/do/run-store.ts) with defaults:
|
|
@@ -144,9 +205,49 @@ export const baseEnvSchema = z.object({
|
|
|
144
205
|
TOOL_RESULT_TTL_HOURS: z.string().optional(),
|
|
145
206
|
TOOL_RESULT_R2_MIN_BYTES: z.string().optional(),
|
|
146
207
|
|
|
208
|
+
// --- chat delivery (docs/chat-delivery.md) -------------------------------
|
|
209
|
+
/**
|
|
210
|
+
* `true` gives Matrix room turns the chat style (several short messages,
|
|
211
|
+
* long content as artefacts); unset keeps `OracleConfig.delivery.matrixChat`,
|
|
212
|
+
* which defaults to one message per reply.
|
|
213
|
+
*/
|
|
214
|
+
MATRIX_CHAT_DELIVERY: z.enum(['true', 'false']).optional(),
|
|
215
|
+
/**
|
|
216
|
+
* Artefact links (the bucket is a binding, `ARTIFACT_BUCKET`): the oracle's
|
|
217
|
+
* public origin, an optional shared viewer page, and the link lifetime in
|
|
218
|
+
* days. Links carry the decryption key, so both URLs must be https (http
|
|
219
|
+
* only on localhost).
|
|
220
|
+
*/
|
|
221
|
+
ORACLE_PUBLIC_URL: secureUrlSchema.optional(),
|
|
222
|
+
ARTIFACT_VIEWER_URL: secureUrlSchema.optional(),
|
|
223
|
+
ARTIFACT_LINK_TTL_DAYS: z.coerce
|
|
224
|
+
.number()
|
|
225
|
+
.int()
|
|
226
|
+
.min(1)
|
|
227
|
+
.max(MAX_ARTIFACT_TTL_DAYS)
|
|
228
|
+
.optional(),
|
|
229
|
+
/**
|
|
230
|
+
* Hours between the cron's sweeps of expired share copies in the bucket
|
|
231
|
+
* (unset = 24). Only runs where `ARTIFACT_BUCKET` is bound and a cron
|
|
232
|
+
* trigger is declared.
|
|
233
|
+
*/
|
|
234
|
+
ARTIFACT_SWEEP_INTERVAL_HOURS: z.coerce
|
|
235
|
+
.number()
|
|
236
|
+
.int()
|
|
237
|
+
.min(1)
|
|
238
|
+
.max(MAX_ARTIFACT_SWEEP_INTERVAL_HOURS)
|
|
239
|
+
.optional(),
|
|
240
|
+
|
|
147
241
|
// --- misc ---------------------------------------------------------------
|
|
148
242
|
LOG_LEVEL: z.string().default('info'),
|
|
149
243
|
CORS_ORIGIN: z.string().default('*'),
|
|
244
|
+
/**
|
|
245
|
+
* Comma-separated DIDs allowed onto the gateway-wide operator routes
|
|
246
|
+
* (`/debug/matrix/*`). Unset: any authenticated caller (with
|
|
247
|
+
* `ORACLE_DEBUG_ROUTES=true`). A list with an entry that is not a DID
|
|
248
|
+
* admits nobody.
|
|
249
|
+
*/
|
|
250
|
+
ORACLE_OPERATOR_DIDS: z.string().optional(),
|
|
150
251
|
});
|
|
151
252
|
|
|
152
253
|
export type BaseEnv = z.infer<typeof baseEnvSchema>;
|
|
@@ -207,8 +308,18 @@ export function composeEnvSchema(
|
|
|
207
308
|
if (!plugin.configSchema) continue;
|
|
208
309
|
const shape = plugin.configSchema.shape as Record<string, unknown>;
|
|
209
310
|
for (const key of Object.keys(shape)) {
|
|
311
|
+
// Runtime-only secrets (the feedback sink's Linear key and pseudonym
|
|
312
|
+
// secret) must never reach a plugin's `ctx.config`.
|
|
313
|
+
if (key.startsWith(FEEDBACK_ENV_PREFIX))
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Plugin '${plugin.name}' declares env key '${key}': the '${FEEDBACK_ENV_PREFIX}' prefix is reserved for the runtime`,
|
|
316
|
+
);
|
|
210
317
|
const previous = pluginOwnership.get(key);
|
|
211
318
|
if (previous !== undefined && previous !== plugin.name) {
|
|
319
|
+
// Plugins that declare the very same schema object share the key on
|
|
320
|
+
// purpose (the wallet-signing chain ids): nothing is replaced, and
|
|
321
|
+
// the first declarer stays its owner.
|
|
322
|
+
if (merged.shape[key] === shape[key]) continue;
|
|
212
323
|
logger.warn(
|
|
213
324
|
`[boot] env key '${key}' is defined by both '${previous}' and '${plugin.name}'; '${plugin.name}' wins.`,
|
|
214
325
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const TASK_EXECUTION_PROFILES = ['supplied-context-markdown'] as const;
|
|
2
|
+
|
|
3
|
+
export type TaskExecutionProfile = (typeof TASK_EXECUTION_PROFILES)[number];
|
|
4
|
+
|
|
5
|
+
export function isTaskExecutionProfile(
|
|
6
|
+
value: unknown,
|
|
7
|
+
): value is TaskExecutionProfile {
|
|
8
|
+
return (TASK_EXECUTION_PROFILES as readonly unknown[]).includes(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function taskExecutionProfile(
|
|
12
|
+
value: unknown,
|
|
13
|
+
): TaskExecutionProfile | undefined {
|
|
14
|
+
if (value === undefined || value === null) return undefined;
|
|
15
|
+
if (isTaskExecutionProfile(value)) return value;
|
|
16
|
+
throw new Error('Unsupported task execution profile');
|
|
17
|
+
}
|