@ixo/oracle-runtime-workers 0.13.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 +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +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 +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +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 +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +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
|
@@ -87,7 +87,13 @@ export interface ComposePromptInput {
|
|
|
87
87
|
userPreferencesContext: string;
|
|
88
88
|
/** Memory-engine context blocks. */
|
|
89
89
|
userContext: UserContextData | undefined;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Date context (`formatDateContext`): the day and the user's timezone. Only
|
|
92
|
+
* values stable for a whole day belong here — the system prompt opens
|
|
93
|
+
* every request, so anything that changes per turn would stop the
|
|
94
|
+
* provider's prompt cache at this point on every turn. The exact time
|
|
95
|
+
* rides on the turn's own message (`renderTurnTimeNote`).
|
|
96
|
+
*/
|
|
91
97
|
timeContext: string;
|
|
92
98
|
/** Currently-viewed entity DID, if any. */
|
|
93
99
|
currentEntityDid: string;
|
|
@@ -95,8 +101,22 @@ export interface ComposePromptInput {
|
|
|
95
101
|
oracleNameOverride?: string;
|
|
96
102
|
/** Degraded-services notice appended after the main prompt body. */
|
|
97
103
|
degradedServicesBlock?: string;
|
|
104
|
+
/**
|
|
105
|
+
* How to write for the surface the reply lands on (chat delivery). Empty on
|
|
106
|
+
* Portal turns, which render a streamed document.
|
|
107
|
+
*/
|
|
108
|
+
surfaceBlock?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Operating guides of the plugins the turn starts with in use, already in
|
|
111
|
+
* their final order (`OraclePlugin.operatingGuide`). Rendered after every
|
|
112
|
+
* other section, under one heading; empty renders nothing.
|
|
113
|
+
*/
|
|
114
|
+
operatingGuides?: readonly string[];
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
/** Heading of the section that carries the in-use plugins' operating guides. */
|
|
118
|
+
export const OPERATING_GUIDES_HEADING = '## Capabilities in use';
|
|
119
|
+
|
|
100
120
|
/** Headers used for each populated memory-context sub-section. */
|
|
101
121
|
const CONTEXT_SECTION_LABELS: Record<ContextSlot, string> = {
|
|
102
122
|
identity: 'Personal identity',
|
|
@@ -403,7 +423,7 @@ const TEMPLATE = `{{{ORACLE_SECTION}}}
|
|
|
403
423
|
{{/DISCOVERY_PRINCIPLE}}
|
|
404
424
|
- Being proactive does **not** mean charging ahead blind. Pause and ask the user a short clarifying question when: (a) the request has multiple plausible interpretations and picking wrong would waste their time, (b) a capability scan surfaces several equally-good fits and you can't tell which they want, (c) you're about to take an irreversible or costly action (deleting data, sending a message, publishing, spending tokens on a long job), or (d) a required input is missing and you'd have to guess. One clarifying question beats five minutes of wrong work — but don't ask for things you can reasonably infer from context.
|
|
405
425
|
- When a tool or sub-agent succeeds, report the result. Never refuse after a successful tool call — including for credentials, tokens, identity data, or block/document operations.
|
|
406
|
-
- When a tool or sub-agent fails, surface the failure to the user and ask how to proceed.
|
|
426
|
+
- The runtime retries a transient failure of a read-only tool once by itself. When a tool or sub-agent still fails, surface the failure to the user and ask how to proceed. Never repeat a write whose outcome is unknown without first verifying what happened, and never claim an authorization you were not given.
|
|
407
427
|
- When delegating to a sub-agent, give it scope, intent, and the context it needs — sub-agents have no access to your conversation history.
|
|
408
428
|
- Match the user's communication style. Be concise. Use Unicode emoji directly (\`🔥\`), never text shortcodes (\`:fire:\`).
|
|
409
429
|
- When a capability turns out to be a strong fit for the user's recurring work, surface it explicitly and — if memory tools are available — offer to save the pattern for next time. Don't save silently; ask first.
|
|
@@ -412,6 +432,10 @@ const TEMPLATE = `{{{ORACLE_SECTION}}}
|
|
|
412
432
|
|
|
413
433
|
{{{COMMUNICATION_STYLE}}}
|
|
414
434
|
{{/COMMUNICATION_STYLE}}
|
|
435
|
+
{{#SURFACE_BLOCK}}
|
|
436
|
+
|
|
437
|
+
{{{SURFACE_BLOCK}}}
|
|
438
|
+
{{/SURFACE_BLOCK}}
|
|
415
439
|
{{#CUSTOM_INSTRUCTIONS}}
|
|
416
440
|
|
|
417
441
|
## Custom Instructions
|
|
@@ -425,7 +449,7 @@ const TEMPLATE = `{{{ORACLE_SECTION}}}
|
|
|
425
449
|
{{{CONTEXT_BLOCK}}}
|
|
426
450
|
{{/CONTEXT_BLOCK}}
|
|
427
451
|
|
|
428
|
-
**Current
|
|
452
|
+
**Current date:** {{TIME_CONTEXT}}
|
|
429
453
|
{{#CURRENT_ENTITY_DID}}
|
|
430
454
|
|
|
431
455
|
**Current entity:** {{CURRENT_ENTITY_DID}}
|
|
@@ -479,6 +503,7 @@ export async function composePrompt(
|
|
|
479
503
|
input.capabilityDiscovery === false ? '' : DISCOVERY_PRINCIPLE,
|
|
480
504
|
CUSTOM_INSTRUCTIONS: input.customInstructions,
|
|
481
505
|
COMMUNICATION_STYLE: communicationStyle,
|
|
506
|
+
SURFACE_BLOCK: input.surfaceBlock ?? '',
|
|
482
507
|
CONTEXT_BLOCK: buildContextBlock(input.userContext),
|
|
483
508
|
TIME_CONTEXT: input.timeContext,
|
|
484
509
|
CURRENT_ENTITY_DID: input.currentEntityDid,
|
|
@@ -486,15 +511,95 @@ export async function composePrompt(
|
|
|
486
511
|
USER_PREFERENCES_CONTEXT: input.userPreferencesContext,
|
|
487
512
|
});
|
|
488
513
|
|
|
514
|
+
// After the stable sections: the set changes only when a plugin is loaded,
|
|
515
|
+
// so the prefix before it stays cacheable across that change too.
|
|
516
|
+
const guides = (input.operatingGuides ?? []).filter((g) => g.trim() !== '');
|
|
517
|
+
const withGuides =
|
|
518
|
+
guides.length > 0
|
|
519
|
+
? `${rendered}\n${OPERATING_GUIDES_HEADING}\n\n${guides.join('\n\n')}\n`
|
|
520
|
+
: rendered;
|
|
521
|
+
|
|
489
522
|
if (input.degradedServicesBlock && input.degradedServicesBlock.length > 0) {
|
|
490
|
-
return `${
|
|
523
|
+
return `${withGuides}\n\n---\n\n## Degraded services\n\n${input.degradedServicesBlock}\n`;
|
|
524
|
+
}
|
|
525
|
+
return withGuides;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
interface ZonedParts {
|
|
529
|
+
weekday: string;
|
|
530
|
+
date: string;
|
|
531
|
+
time: string;
|
|
532
|
+
/** The IANA zone the parts are in: the one asked for, or `UTC` when it is not a zone. */
|
|
533
|
+
zone: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** `instant` as calendar parts in `timeZone`, or in UTC when that is not a usable zone. */
|
|
537
|
+
function zonedParts(instant: Date, timeZone: string | undefined): ZonedParts {
|
|
538
|
+
const zone = timeZone?.trim() || 'UTC';
|
|
539
|
+
let parts: Intl.DateTimeFormatPart[];
|
|
540
|
+
try {
|
|
541
|
+
parts = new Intl.DateTimeFormat('en-US', {
|
|
542
|
+
timeZone: zone,
|
|
543
|
+
weekday: 'long',
|
|
544
|
+
year: 'numeric',
|
|
545
|
+
month: '2-digit',
|
|
546
|
+
day: '2-digit',
|
|
547
|
+
hour: '2-digit',
|
|
548
|
+
minute: '2-digit',
|
|
549
|
+
hourCycle: 'h23',
|
|
550
|
+
}).formatToParts(instant);
|
|
551
|
+
} catch {
|
|
552
|
+
if (zone === 'UTC') throw new RangeError('Invalid date');
|
|
553
|
+
return zonedParts(instant, 'UTC');
|
|
491
554
|
}
|
|
492
|
-
|
|
555
|
+
const part = (type: Intl.DateTimeFormatPartTypes): string =>
|
|
556
|
+
parts.find((p) => p.type === type)?.value ?? '';
|
|
557
|
+
return {
|
|
558
|
+
weekday: part('weekday'),
|
|
559
|
+
date: `${part('year')}-${part('month')}-${part('day')}`,
|
|
560
|
+
time: `${part('hour')}:${part('minute')}`,
|
|
561
|
+
zone,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function parseInstant(value: string | undefined): Date | undefined {
|
|
566
|
+
if (!value) return undefined;
|
|
567
|
+
const instant = new Date(value);
|
|
568
|
+
return Number.isNaN(instant.getTime()) ? undefined : instant;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* The system prompt's date line: the day in the user's timezone and the
|
|
573
|
+
* zone (`Saturday, 2026-10-03 (Europe/Berlin)`). Nothing finer than the day,
|
|
574
|
+
* so the prompt stays byte-identical across the turns of a day. A timezone
|
|
575
|
+
* that is not an IANA zone is not rendered; the day is then UTC's.
|
|
576
|
+
*/
|
|
577
|
+
export function formatDateContext(
|
|
578
|
+
timezone: string | undefined,
|
|
579
|
+
currentTime: string | undefined,
|
|
580
|
+
): string {
|
|
581
|
+
const instant = parseInstant(currentTime);
|
|
582
|
+
if (!instant) return 'Not available.';
|
|
583
|
+
const { weekday, date, zone } = zonedParts(instant, timezone);
|
|
584
|
+
return `${weekday}, ${date} (${zone})`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The exact time for the turn's own message (`Current time: Saturday,
|
|
589
|
+
* 2026-10-03 14:05 (Europe/Berlin)`): it belongs on the message the user
|
|
590
|
+
* sent this turn, where it is part of the history from then on, not in the
|
|
591
|
+
* system prompt that opens every request. Pure: same inputs, same text.
|
|
592
|
+
*/
|
|
593
|
+
export function renderTurnTimeNote(now: Date, timezone?: string): string {
|
|
594
|
+
if (Number.isNaN(now.getTime())) return 'Current time: not available.';
|
|
595
|
+
const { weekday, date, time, zone } = zonedParts(now, timezone);
|
|
596
|
+
return `Current time: ${weekday}, ${date} ${time} (${zone})`;
|
|
493
597
|
}
|
|
494
598
|
|
|
495
599
|
/**
|
|
496
|
-
* Format a (timezone, currentTime) pair into a
|
|
497
|
-
* forks driving their own `composePrompt` calls
|
|
600
|
+
* Format a (timezone, currentTime) pair into a prompt block, time included.
|
|
601
|
+
* Kept for forks driving their own `composePrompt` calls; the runtime's own
|
|
602
|
+
* prompt uses `formatDateContext`, which does not change within a day.
|
|
498
603
|
*/
|
|
499
604
|
export function formatTimeContext(
|
|
500
605
|
timezone: string | undefined,
|
package/src/core/registries.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DecisionRegistration } from '@ixo/common/ai/decisions';
|
|
1
2
|
import type { z } from 'zod';
|
|
2
3
|
import type { OraclePlugin } from '../plugin-api/oracle-plugin';
|
|
3
4
|
import type {
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
import { computeSubAgentToolName } from './subagent-as-tool';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* The
|
|
21
|
+
* The seven registries the runtime composes. Instances are created per
|
|
21
22
|
* `createRuntimeCore()` call — never at module scope — so an isolate that
|
|
22
23
|
* boots two oracles (tests, or a multi-tenant shell) never shares caches.
|
|
23
24
|
*/
|
|
@@ -28,6 +29,7 @@ export interface Registries {
|
|
|
28
29
|
manifests: ManifestRegistry;
|
|
29
30
|
configSchema: ConfigSchemaRegistry;
|
|
30
31
|
sharedState: SharedStateRegistry;
|
|
32
|
+
decisions: DecisionRegistry;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** Build a fresh, empty set of registries. */
|
|
@@ -39,6 +41,7 @@ export function createRegistries(): Registries {
|
|
|
39
41
|
manifests: new ManifestRegistry(),
|
|
40
42
|
configSchema: new ConfigSchemaRegistry(),
|
|
41
43
|
sharedState: new SharedStateRegistry(),
|
|
44
|
+
decisions: new DecisionRegistry(),
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -83,6 +86,31 @@ export interface ToolSummary {
|
|
|
83
86
|
origin: 'boot' | 'request';
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
/** Where `load_capability` reads the tools a plugin contributes. */
|
|
90
|
+
export interface ToolSummarySource {
|
|
91
|
+
toolSummariesForPlugin(
|
|
92
|
+
pluginName: string,
|
|
93
|
+
): Array<{ name: string; description: string }>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* One turn's view of its own collected tools (boot and request-time), in
|
|
98
|
+
* collection order — never another turn's.
|
|
99
|
+
*/
|
|
100
|
+
export function turnToolSummaries(
|
|
101
|
+
tools: ReadonlyArray<{ pluginName: string; tool: PluginTool }>,
|
|
102
|
+
): ToolSummarySource {
|
|
103
|
+
return {
|
|
104
|
+
toolSummariesForPlugin: (pluginName) =>
|
|
105
|
+
tools
|
|
106
|
+
.filter((entry) => entry.pluginName === pluginName)
|
|
107
|
+
.map(({ tool }) => ({
|
|
108
|
+
name: tool.name,
|
|
109
|
+
description: tool.description,
|
|
110
|
+
})),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
/**
|
|
87
115
|
* Stores plugins that contribute tools and resolves them lazily by invoking
|
|
88
116
|
* each plugin's `getTools(buildCtx)` once at collection time.
|
|
@@ -105,19 +133,11 @@ export interface ToolSummary {
|
|
|
105
133
|
export class ToolRegistry {
|
|
106
134
|
private readonly plugins: OraclePlugin[] = [];
|
|
107
135
|
private bootCache: RegisteredTool[] | null = null;
|
|
108
|
-
/**
|
|
109
|
-
* Metadata snapshot of the most recent `collect()`. Deliberately NOT the
|
|
110
|
-
* tool objects: request-time tools close over that request's
|
|
111
|
-
* `RuntimeContext`, and retaining them here would pin a full request
|
|
112
|
-
* graph in memory between turns.
|
|
113
|
-
*/
|
|
114
|
-
private collectedMeta: ToolSummary[] | null = null;
|
|
115
136
|
|
|
116
137
|
/** Add a plugin whose `getTools` will be called at `collect()` time. */
|
|
117
138
|
register(plugin: OraclePlugin): void {
|
|
118
139
|
this.plugins.push(plugin);
|
|
119
140
|
this.bootCache = null;
|
|
120
|
-
this.collectedMeta = null;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
/**
|
|
@@ -184,6 +204,11 @@ export class ToolRegistry {
|
|
|
184
204
|
* Combined boot + request collection used by the main agent build. Uses
|
|
185
205
|
* the cached boot output when present so per-request rebuilds skip the
|
|
186
206
|
* `getTools` invocations that don't depend on runtime state.
|
|
207
|
+
*
|
|
208
|
+
* The request-time part is returned to the caller only, never kept: the
|
|
209
|
+
* registry is shared by every turn of the isolate, so nothing one user's
|
|
210
|
+
* turn collected may be read back by another (`turnToolSummaries` gives a
|
|
211
|
+
* turn its own view).
|
|
187
212
|
*/
|
|
188
213
|
async collect(
|
|
189
214
|
buildCtx: PluginContext,
|
|
@@ -191,19 +216,13 @@ export class ToolRegistry {
|
|
|
191
216
|
): Promise<RegisteredTool[]> {
|
|
192
217
|
const boot = await this.collectBoot(buildCtx);
|
|
193
218
|
const request = rtCtx ? await this.collectRequest(rtCtx) : [];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
name: tool.name,
|
|
198
|
-
description: tool.description,
|
|
199
|
-
origin,
|
|
200
|
-
}));
|
|
201
|
-
return out;
|
|
219
|
+
return rtCtx
|
|
220
|
+
? withoutShadowingRequestTools([...boot, ...request], rtCtx.logger)
|
|
221
|
+
: boot;
|
|
202
222
|
}
|
|
203
223
|
|
|
204
|
-
/** Summaries
|
|
224
|
+
/** Summaries of the boot-time tools (the boot checks read these). */
|
|
205
225
|
private summaries(): ToolSummary[] {
|
|
206
|
-
if (this.collectedMeta !== null) return this.collectedMeta;
|
|
207
226
|
return (this.bootCache ?? []).map(({ pluginName, tool, origin }) => ({
|
|
208
227
|
pluginName,
|
|
209
228
|
name: tool.name,
|
|
@@ -212,15 +231,12 @@ export class ToolRegistry {
|
|
|
212
231
|
}));
|
|
213
232
|
}
|
|
214
233
|
|
|
215
|
-
/** The flat list of tool names
|
|
234
|
+
/** The flat list of boot-time tool names. */
|
|
216
235
|
toolNames(): string[] {
|
|
217
236
|
return this.summaries().map((entry) => entry.name);
|
|
218
237
|
}
|
|
219
238
|
|
|
220
|
-
/**
|
|
221
|
-
* Tool names contributed by a given plugin in the most recent `collect()`
|
|
222
|
-
* (or `collectBoot()` if no full collection has happened yet).
|
|
223
|
-
*/
|
|
239
|
+
/** Boot-time tool names contributed by a given plugin. */
|
|
224
240
|
toolNamesForPlugin(pluginName: string): string[] {
|
|
225
241
|
return this.summaries()
|
|
226
242
|
.filter((entry) => entry.pluginName === pluginName)
|
|
@@ -228,8 +244,9 @@ export class ToolRegistry {
|
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
/**
|
|
231
|
-
* Name/description of the tools a given plugin contributed
|
|
232
|
-
*
|
|
247
|
+
* Name/description of the boot-time tools a given plugin contributed.
|
|
248
|
+
* Summaries, not the tool objects. A turn that also has request-time tools
|
|
249
|
+
* reads its own collection instead (`turnToolSummaries`).
|
|
233
250
|
*/
|
|
234
251
|
toolSummariesForPlugin(
|
|
235
252
|
pluginName: string,
|
|
@@ -245,7 +262,7 @@ export class ToolRegistry {
|
|
|
245
262
|
* actual conflict.
|
|
246
263
|
*/
|
|
247
264
|
assertNoCollisions(): void {
|
|
248
|
-
if (this.
|
|
265
|
+
if (this.bootCache === null) {
|
|
249
266
|
throw new Error('ToolRegistry.assertNoCollisions called before collect');
|
|
250
267
|
}
|
|
251
268
|
const seen = new Map<string, string>();
|
|
@@ -268,6 +285,86 @@ export class ToolRegistry {
|
|
|
268
285
|
}
|
|
269
286
|
}
|
|
270
287
|
|
|
288
|
+
/**
|
|
289
|
+
* A request-time tool (most of them declared by the client in the turn body,
|
|
290
|
+
* such as the Portal's browser tools) may not take a name another tool of the
|
|
291
|
+
* turn already has: the model would call one name while the runtime ran
|
|
292
|
+
* either tool. The colliding request tool is dropped for this turn with one
|
|
293
|
+
* warning — the server tool (or the first request tool of that name) stays,
|
|
294
|
+
* and the turn runs. Failing the turn instead would break every turn of a
|
|
295
|
+
* client release that happens to declare a clashing name. Boot-time
|
|
296
|
+
* collisions are `assertNoCollisions`' job; the names the registry cannot
|
|
297
|
+
* see (the meta-tools, the runtime's per-turn tools, sub-agent tool names)
|
|
298
|
+
* are reserved by `dropShadowingRequestEntries` when the agent is built.
|
|
299
|
+
*/
|
|
300
|
+
function withoutShadowingRequestTools(
|
|
301
|
+
tools: readonly RegisteredTool[],
|
|
302
|
+
logger: { warn(message: string): void },
|
|
303
|
+
): RegisteredTool[] {
|
|
304
|
+
const owner = new Map<string, string>();
|
|
305
|
+
const out: RegisteredTool[] = [];
|
|
306
|
+
for (const entry of tools) {
|
|
307
|
+
const { pluginName, tool, origin } = entry;
|
|
308
|
+
const previous = owner.get(tool.name);
|
|
309
|
+
if (previous !== undefined && origin === 'request') {
|
|
310
|
+
logger.warn(
|
|
311
|
+
`${LOG_PREFIX} request tool "${tool.name}" of plugin "${pluginName}" dropped for this turn: the name is already taken by plugin "${previous}"`,
|
|
312
|
+
);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (previous === undefined) owner.set(tool.name, pluginName);
|
|
316
|
+
out.push(entry);
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Drop the request-time tools and sub-agents whose name is already taken.
|
|
323
|
+
* Request-time contributions are recomputed every turn and some carry names
|
|
324
|
+
* a client declared (browser tools, AG-UI actions), so none of them may stand
|
|
325
|
+
* in for a tool the server owns: a name in `reservedNames` (the turn's
|
|
326
|
+
* boot-time tools and sub-agents, the meta-tools and the runtime's per-turn
|
|
327
|
+
* tools), or one an earlier request-time entry of the same turn already
|
|
328
|
+
* claimed, is dropped with one warning naming the plugin. Sub-agents are
|
|
329
|
+
* compared by the tool name they are bound under. Boot-time entries pass
|
|
330
|
+
* through untouched; their collisions are a boot error
|
|
331
|
+
* (`assertNoCollisions`).
|
|
332
|
+
*/
|
|
333
|
+
export function dropShadowingRequestEntries(opts: {
|
|
334
|
+
tools: readonly RegisteredTool[];
|
|
335
|
+
requestSubAgents: readonly RegisteredSubAgent[];
|
|
336
|
+
reservedNames: Iterable<string>;
|
|
337
|
+
logger: { warn(message: string): void };
|
|
338
|
+
}): { tools: RegisteredTool[]; requestSubAgents: RegisteredSubAgent[] } {
|
|
339
|
+
const taken = new Set(opts.reservedNames);
|
|
340
|
+
const drop = (pluginName: string, name: string, kind: string): void =>
|
|
341
|
+
opts.logger.warn(
|
|
342
|
+
`${LOG_PREFIX} plugin "${pluginName}" request-time ${kind} "${name}" dropped: the name is already taken this turn`,
|
|
343
|
+
);
|
|
344
|
+
const tools: RegisteredTool[] = [];
|
|
345
|
+
for (const entry of opts.tools) {
|
|
346
|
+
if (entry.origin === 'request') {
|
|
347
|
+
if (taken.has(entry.tool.name)) {
|
|
348
|
+
drop(entry.pluginName, entry.tool.name, 'tool');
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
taken.add(entry.tool.name);
|
|
352
|
+
}
|
|
353
|
+
tools.push(entry);
|
|
354
|
+
}
|
|
355
|
+
const requestSubAgents: RegisteredSubAgent[] = [];
|
|
356
|
+
for (const entry of opts.requestSubAgents) {
|
|
357
|
+
const toolName = computeSubAgentToolName(entry.subAgent.name);
|
|
358
|
+
if (taken.has(toolName)) {
|
|
359
|
+
drop(entry.pluginName, toolName, 'sub-agent');
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
taken.add(toolName);
|
|
363
|
+
requestSubAgents.push(entry);
|
|
364
|
+
}
|
|
365
|
+
return { tools, requestSubAgents };
|
|
366
|
+
}
|
|
367
|
+
|
|
271
368
|
// ── Sub-agent registry ──────────────────────────────────────────────────────
|
|
272
369
|
|
|
273
370
|
/** A collected sub-agent tagged with the plugin that contributed it. */
|
|
@@ -286,17 +383,11 @@ export interface RegisteredSubAgent {
|
|
|
286
383
|
export class SubAgentRegistry {
|
|
287
384
|
private readonly plugins: OraclePlugin[] = [];
|
|
288
385
|
private bootCache: RegisteredSubAgent[] | null = null;
|
|
289
|
-
/** Names-only snapshot of the most recent `collect()` (see ToolRegistry). */
|
|
290
|
-
private collectedNames: Array<{
|
|
291
|
-
pluginName: string;
|
|
292
|
-
subAgentName: string;
|
|
293
|
-
}> | null = null;
|
|
294
386
|
|
|
295
387
|
/** Add a plugin whose `getSubAgents` will be called at `collect()` time. */
|
|
296
388
|
register(plugin: OraclePlugin): void {
|
|
297
389
|
this.plugins.push(plugin);
|
|
298
390
|
this.bootCache = null;
|
|
299
|
-
this.collectedNames = null;
|
|
300
391
|
}
|
|
301
392
|
|
|
302
393
|
/** Run every plugin's `getSubAgents(buildCtx)` once and cache the result. */
|
|
@@ -343,24 +434,21 @@ export class SubAgentRegistry {
|
|
|
343
434
|
return perPlugin.flat();
|
|
344
435
|
}
|
|
345
436
|
|
|
346
|
-
/**
|
|
437
|
+
/**
|
|
438
|
+
* Combined boot + request collection used by the main agent build. As
|
|
439
|
+
* with `ToolRegistry.collect`, the request-time part is not kept.
|
|
440
|
+
*/
|
|
347
441
|
async collect(
|
|
348
442
|
buildCtx: PluginContext,
|
|
349
443
|
rtCtx?: RuntimeContext,
|
|
350
444
|
): Promise<RegisteredSubAgent[]> {
|
|
351
445
|
const boot = this.collectBoot(buildCtx);
|
|
352
446
|
const request = rtCtx ? await this.collectRequest(rtCtx) : [];
|
|
353
|
-
|
|
354
|
-
this.collectedNames = out.map(({ pluginName, subAgent }) => ({
|
|
355
|
-
pluginName,
|
|
356
|
-
subAgentName: subAgent.name,
|
|
357
|
-
}));
|
|
358
|
-
return out;
|
|
447
|
+
return [...boot, ...request];
|
|
359
448
|
}
|
|
360
449
|
|
|
361
|
-
/** Names
|
|
450
|
+
/** Names of the boot-time sub-agents (the boot checks read these). */
|
|
362
451
|
private names(): Array<{ pluginName: string; subAgentName: string }> {
|
|
363
|
-
if (this.collectedNames !== null) return this.collectedNames;
|
|
364
452
|
return (this.bootCache ?? []).map(({ pluginName, subAgent }) => ({
|
|
365
453
|
pluginName,
|
|
366
454
|
subAgentName: subAgent.name,
|
|
@@ -368,8 +456,8 @@ export class SubAgentRegistry {
|
|
|
368
456
|
}
|
|
369
457
|
|
|
370
458
|
/**
|
|
371
|
-
* The *wrapped* tool names
|
|
372
|
-
*
|
|
459
|
+
* The *wrapped* tool names of the boot-time sub-agents a given plugin
|
|
460
|
+
* contributes. Each entry passes through `computeSubAgentToolName` — the
|
|
373
461
|
* same transform `createSubagentAsTool` applies — so the names returned
|
|
374
462
|
* here match what the agent will actually see.
|
|
375
463
|
*/
|
|
@@ -384,7 +472,7 @@ export class SubAgentRegistry {
|
|
|
384
472
|
* message names both plugin names so the boot log points at the conflict.
|
|
385
473
|
*/
|
|
386
474
|
assertNoCollisions(): void {
|
|
387
|
-
if (this.
|
|
475
|
+
if (this.bootCache === null) {
|
|
388
476
|
throw new Error(
|
|
389
477
|
'SubAgentRegistry.assertNoCollisions called before collect',
|
|
390
478
|
);
|
|
@@ -424,8 +512,6 @@ export interface RegisteredMiddleware {
|
|
|
424
512
|
* Order is preserved as registration order — the loader applies any
|
|
425
513
|
* dependency-driven topological reordering before registering plugins here.
|
|
426
514
|
*
|
|
427
|
-
* Middlewares are boot-time-only — there's no per-request hook. The
|
|
428
|
-
* registry caches the first `collect(buildCtx)` result.
|
|
429
515
|
*/
|
|
430
516
|
export class MiddlewareRegistry {
|
|
431
517
|
private readonly plugins: OraclePlugin[] = [];
|
|
@@ -450,6 +536,17 @@ export class MiddlewareRegistry {
|
|
|
450
536
|
return out;
|
|
451
537
|
}
|
|
452
538
|
|
|
539
|
+
async collectRequest(ctx: RuntimeContext): Promise<RegisteredMiddleware[]> {
|
|
540
|
+
const entries = await Promise.all(
|
|
541
|
+
this.plugins.map(async (plugin) =>
|
|
542
|
+
((await plugin.getRequestMiddlewares?.(ctx)) ?? []).map(
|
|
543
|
+
(middleware) => ({ pluginName: plugin.name, middleware }),
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
return entries.flat();
|
|
548
|
+
}
|
|
549
|
+
|
|
453
550
|
/** Middlewares have no names, so no collision is possible. */
|
|
454
551
|
assertNoCollisions(): void {
|
|
455
552
|
// Intentional no-op — middleware order is established by the loader.
|
|
@@ -475,17 +572,35 @@ export interface ManifestCrossCheckResult {
|
|
|
475
572
|
*/
|
|
476
573
|
export class ManifestRegistry {
|
|
477
574
|
private readonly entries: RegisteredManifest[] = [];
|
|
575
|
+
/** Plugin name → `operatingGuide`, kept apart from the manifests (never rendered with them). */
|
|
576
|
+
private readonly guides = new Map<string, string>();
|
|
478
577
|
|
|
479
578
|
/**
|
|
480
579
|
* Record a plugin's manifest. A fork-supplied `override` is merged shallowly
|
|
481
580
|
* over the plugin's own manifest, so every downstream reader sees the
|
|
482
|
-
* effective manifest.
|
|
581
|
+
* effective manifest. A non-blank `operatingGuide` is recorded beside it.
|
|
483
582
|
*/
|
|
484
583
|
register(plugin: OraclePlugin, override?: PluginManifestOverride): void {
|
|
485
584
|
this.entries.push({
|
|
486
585
|
pluginName: plugin.name,
|
|
487
586
|
manifest: mergeManifestOverride(plugin.manifest, override),
|
|
488
587
|
});
|
|
588
|
+
const guide = plugin.operatingGuide?.trim();
|
|
589
|
+
if (guide) this.guides.set(plugin.name, guide);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** The plugin's operating guide, trimmed; `undefined` when it has none. */
|
|
593
|
+
operatingGuide(pluginName: string): string | undefined {
|
|
594
|
+
return this.guides.get(pluginName);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Every registered operating guide, by plugin name in code-point order. */
|
|
598
|
+
operatingGuides(): Array<{ pluginName: string; guide: string }> {
|
|
599
|
+
return [...this.guides]
|
|
600
|
+
.map(([pluginName, guide]) => ({ pluginName, guide }))
|
|
601
|
+
.sort((a, b) =>
|
|
602
|
+
a.pluginName < b.pluginName ? -1 : a.pluginName > b.pluginName ? 1 : 0,
|
|
603
|
+
);
|
|
489
604
|
}
|
|
490
605
|
|
|
491
606
|
/** Registered manifests in registration order. */
|
|
@@ -640,3 +755,93 @@ export class SharedStateRegistry {
|
|
|
640
755
|
}
|
|
641
756
|
}
|
|
642
757
|
}
|
|
758
|
+
|
|
759
|
+
// ── Decision registry ───────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
/** A collected decision tagged with the plugin that contributed it. */
|
|
762
|
+
export interface RegisteredDecision {
|
|
763
|
+
pluginName: string;
|
|
764
|
+
decision: DecisionRegistration;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
interface DecisionBootCache {
|
|
768
|
+
entries: RegisteredDecision[];
|
|
769
|
+
/** First registration per name; duplicates surface in `assertNoCollisions`. */
|
|
770
|
+
byName: Map<string, RegisteredDecision>;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Stores the bounded semantic decisions plugins contribute through
|
|
775
|
+
* `getDecisions(ctx)`. Collected once per isolate (in `warm()`); the
|
|
776
|
+
* `DecisionRuntime` resolves `evaluateByName` through `get`. Name collisions
|
|
777
|
+
* across plugins are a boot error.
|
|
778
|
+
*/
|
|
779
|
+
export class DecisionRegistry {
|
|
780
|
+
private readonly plugins: OraclePlugin[] = [];
|
|
781
|
+
private bootCache: DecisionBootCache | null = null;
|
|
782
|
+
|
|
783
|
+
register(plugin: OraclePlugin): void {
|
|
784
|
+
this.plugins.push(plugin);
|
|
785
|
+
this.bootCache = null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Collect every plugin's decisions once; later calls return the cache. */
|
|
789
|
+
collect(buildCtx: PluginContext): RegisteredDecision[] {
|
|
790
|
+
if (this.bootCache !== null) return this.bootCache.entries;
|
|
791
|
+
|
|
792
|
+
const entries: RegisteredDecision[] = [];
|
|
793
|
+
const byName = new Map<string, RegisteredDecision>();
|
|
794
|
+
for (const plugin of this.plugins) {
|
|
795
|
+
if (!plugin.getDecisions) continue;
|
|
796
|
+
for (const decision of plugin.getDecisions(buildCtx)) {
|
|
797
|
+
const entry: RegisteredDecision = { pluginName: plugin.name, decision };
|
|
798
|
+
entries.push(entry);
|
|
799
|
+
if (!byName.has(decision.name)) byName.set(decision.name, entry);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
this.bootCache = { entries, byName };
|
|
803
|
+
return entries;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
get(name: string): RegisteredDecision | undefined {
|
|
807
|
+
if (this.bootCache === null) {
|
|
808
|
+
throw new Error('DecisionRegistry.get called before collect');
|
|
809
|
+
}
|
|
810
|
+
return this.bootCache.byName.get(name);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
namesForPlugin(pluginName: string): string[] {
|
|
814
|
+
if (this.bootCache === null) return [];
|
|
815
|
+
return this.bootCache.entries
|
|
816
|
+
.filter((entry) => entry.pluginName === pluginName)
|
|
817
|
+
.map((entry) => entry.decision.name);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Throw if two plugins contribute decisions under the same name. */
|
|
821
|
+
assertNoCollisions(): void {
|
|
822
|
+
if (this.bootCache === null) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
'DecisionRegistry.assertNoCollisions called before collect',
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const seen = new Map<string, string>();
|
|
829
|
+
const collisions: string[] = [];
|
|
830
|
+
for (const { pluginName, decision } of this.bootCache.entries) {
|
|
831
|
+
const previous = seen.get(decision.name);
|
|
832
|
+
if (previous && previous !== pluginName) {
|
|
833
|
+
collisions.push(
|
|
834
|
+
`Decision "${decision.name}" registered by both "${previous}" and "${pluginName}"`,
|
|
835
|
+
);
|
|
836
|
+
} else if (!previous) {
|
|
837
|
+
seen.set(decision.name, pluginName);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (collisions.length > 0) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`DecisionRegistry: decision name collisions detected:\n - ${collisions.join('\n - ')}`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|