okengine 0.13.0 → 0.15.2
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/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/fx.mdx +27 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/serve.ts +17 -2
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +10 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
|
@@ -8,11 +8,26 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { AiDriver, AiMessage, AiModelClient, AiToolDef } from "../../drivers/ai-types.ts";
|
|
11
|
+
import { currentAbortSignal, withAbortSignal } from "../../kernel/abort-scope.ts";
|
|
12
|
+
import {
|
|
13
|
+
mcpCapabilityRefFromName,
|
|
14
|
+
mcpModelToolName,
|
|
15
|
+
parseMcpToolRef,
|
|
16
|
+
} from "../../manifest/mcp-ref.ts";
|
|
17
|
+
import { createMcpClient, type McpClient } from "./mcp-client.ts";
|
|
18
|
+
import type { McpTransport } from "./mcp-transport.ts";
|
|
11
19
|
import type { IndexStore } from "../../drivers/types.ts";
|
|
12
20
|
import { maskRedactedDeep } from "../../kernel/redacted.ts";
|
|
13
21
|
import type { GatePolicyContext } from "../gate/declare.ts";
|
|
14
22
|
import type { GateRuntime } from "../gate/runtime.ts";
|
|
15
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
AiAgentDecl,
|
|
25
|
+
AiEmbedDecl,
|
|
26
|
+
AiMcpServerDecl,
|
|
27
|
+
AiModelDecl,
|
|
28
|
+
AiPromptDecl,
|
|
29
|
+
AiTimeout,
|
|
30
|
+
} from "./declare.ts";
|
|
16
31
|
import {
|
|
17
32
|
isRetryableAiError,
|
|
18
33
|
mergeAskAbortSignal,
|
|
@@ -102,7 +117,7 @@ export interface AiFallbackAttempt {
|
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
/** Ask outcome class — schema failure is not a provider error. */
|
|
105
|
-
export type AiAskOutcome = "ok" | "provider_error" | "schema_invalid";
|
|
120
|
+
export type AiAskOutcome = "ok" | "provider_error" | "schema_invalid" | "budget_exceeded";
|
|
106
121
|
|
|
107
122
|
/** Journal entry for a nondeterministic ask. */
|
|
108
123
|
export interface AiJournalEntry {
|
|
@@ -171,6 +186,11 @@ export interface CreateAiRuntimeOptions {
|
|
|
171
186
|
readonly clients?: Readonly<Record<string, AiModelClient>>;
|
|
172
187
|
/** Default driver when a model has no client (usually mock in dev). */
|
|
173
188
|
readonly defaultDriver?: AiDriver;
|
|
189
|
+
/**
|
|
190
|
+
* Protocol drivers keyed by id — used when a logical model sets
|
|
191
|
+
* {@link AiModelDecl.driverId} instead of the app default.
|
|
192
|
+
*/
|
|
193
|
+
readonly drivers?: Readonly<Record<string, AiDriver>>;
|
|
174
194
|
/** Gate runtime for agent tool calls. */
|
|
175
195
|
readonly gates?: GateRuntime;
|
|
176
196
|
/**
|
|
@@ -208,6 +228,12 @@ export interface CreateAiRuntimeOptions {
|
|
|
208
228
|
* Nondeterministic contract.
|
|
209
229
|
*/
|
|
210
230
|
readonly forceJournal?: boolean;
|
|
231
|
+
/** Declared external MCP servers. */
|
|
232
|
+
readonly mcpServers?: readonly AiMcpServerDecl[];
|
|
233
|
+
/** Resolve a vault secret by contract name (MCP bearer). */
|
|
234
|
+
readonly resolveSecret?: (name: string) => string | Promise<string>;
|
|
235
|
+
/** Injected MCP transports keyed by server name (tests). */
|
|
236
|
+
readonly mcpTransports?: Readonly<Record<string, McpTransport>>;
|
|
211
237
|
}
|
|
212
238
|
|
|
213
239
|
/** Ask options. */
|
|
@@ -242,6 +268,8 @@ export interface AiStreamOptions {
|
|
|
242
268
|
readonly prompt?: string;
|
|
243
269
|
readonly data?: unknown;
|
|
244
270
|
readonly signal?: AbortSignal;
|
|
271
|
+
/** Ordered fallback models after the primary `stream(model)` name. */
|
|
272
|
+
readonly via?: readonly string[];
|
|
245
273
|
}
|
|
246
274
|
|
|
247
275
|
/** AI runtime surface. */
|
|
@@ -291,6 +319,14 @@ export interface AiRuntime {
|
|
|
291
319
|
* @param options - Prompt / data / signal
|
|
292
320
|
*/
|
|
293
321
|
stream(model: string, options?: AiStreamOptions): AsyncIterable<string>;
|
|
322
|
+
/**
|
|
323
|
+
* Call an allowlisted MCP tool (`mcp:<server>/<tool>`).
|
|
324
|
+
*
|
|
325
|
+
* @param ref - Capability ref
|
|
326
|
+
* @param input - Tool arguments
|
|
327
|
+
* @param signal - Cancel
|
|
328
|
+
*/
|
|
329
|
+
callMcp(ref: string, input: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
294
330
|
/**
|
|
295
331
|
* Embed text into the configured index store.
|
|
296
332
|
*
|
|
@@ -341,6 +377,11 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
341
377
|
for (const m of options.models ?? []) models.set(m.name, m);
|
|
342
378
|
|
|
343
379
|
const clients = new Map<string, AiModelClient>(Object.entries(options.clients ?? {}));
|
|
380
|
+
const mcpClient: McpClient = createMcpClient({
|
|
381
|
+
servers: options.mcpServers,
|
|
382
|
+
...(options.resolveSecret !== undefined ? { resolveSecret: options.resolveSecret } : {}),
|
|
383
|
+
...(options.mcpTransports !== undefined ? { transports: options.mcpTransports } : {}),
|
|
384
|
+
});
|
|
344
385
|
const denials: AgentDenial[] = [];
|
|
345
386
|
const agentRuns: AgentRunRecord[] = [];
|
|
346
387
|
const journal: AiJournalEntry[] = [];
|
|
@@ -351,11 +392,18 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
351
392
|
async function clientFor(name: string): Promise<AiModelClient> {
|
|
352
393
|
const existing = clients.get(name);
|
|
353
394
|
if (existing) return existing;
|
|
354
|
-
if (!options.defaultDriver) {
|
|
355
|
-
throw new Error(`ai: no client for model "${name}" and no defaultDriver`);
|
|
356
|
-
}
|
|
357
395
|
const model = models.get(name);
|
|
358
|
-
const
|
|
396
|
+
const driver =
|
|
397
|
+
(model?.driverId !== undefined ? options.drivers?.[model.driverId] : undefined) ??
|
|
398
|
+
options.defaultDriver;
|
|
399
|
+
if (!driver) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
model?.driverId
|
|
402
|
+
? `ai: no driver "${model.driverId}" for model "${name}" and no defaultDriver`
|
|
403
|
+
: `ai: no client for model "${name}" and no defaultDriver`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const opened = await driver.open({
|
|
359
407
|
model: model?.model ?? name,
|
|
360
408
|
...(model?.baseUrl !== undefined ? { baseUrl: model.baseUrl } : {}),
|
|
361
409
|
...(model?.apiKey !== undefined ? { apiKey: model.apiKey } : {}),
|
|
@@ -373,12 +421,26 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
373
421
|
return options.effectsForFlow?.(tool) ?? [];
|
|
374
422
|
}
|
|
375
423
|
|
|
376
|
-
function toolDefsFor(toolNames: readonly string[]): AiToolDef[] {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
424
|
+
async function toolDefsFor(toolNames: readonly string[]): Promise<AiToolDef[]> {
|
|
425
|
+
const defs: AiToolDef[] = [];
|
|
426
|
+
for (const name of toolNames) {
|
|
427
|
+
const mcp = parseMcpToolRef(name);
|
|
428
|
+
if (mcp) {
|
|
429
|
+
const listed = await mcpClient.listedTool(mcp);
|
|
430
|
+
defs.push({
|
|
431
|
+
name: mcpModelToolName(mcp.server, mcp.tool),
|
|
432
|
+
description: listed?.description ?? `MCP tool: ${mcp.server}/${mcp.tool}`,
|
|
433
|
+
parameters: listed?.inputSchema ?? { type: "object", properties: {} },
|
|
434
|
+
});
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
defs.push({
|
|
438
|
+
name,
|
|
439
|
+
description: `Flow tool: ${name}`,
|
|
440
|
+
parameters: options.toolSchemaForFlow?.(name) ?? { type: "object", properties: {} },
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return defs;
|
|
382
444
|
}
|
|
383
445
|
|
|
384
446
|
async function dispatchTool(opts: {
|
|
@@ -392,6 +454,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
392
454
|
readonly meta?: GatePolicyContext["meta"];
|
|
393
455
|
readonly trail: AgentToolStep[];
|
|
394
456
|
readonly runDenials: AgentDenial[];
|
|
457
|
+
readonly signal?: AbortSignal;
|
|
395
458
|
}): Promise<unknown> {
|
|
396
459
|
const {
|
|
397
460
|
tool,
|
|
@@ -404,10 +467,12 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
404
467
|
meta,
|
|
405
468
|
trail,
|
|
406
469
|
runDenials,
|
|
470
|
+
signal,
|
|
407
471
|
} = opts;
|
|
408
|
-
const
|
|
472
|
+
const capability = mcpCapabilityRefFromName(tool) ?? tool;
|
|
473
|
+
const effects = effectsFor(capability);
|
|
409
474
|
|
|
410
|
-
if (!allowedTools.has(tool)) {
|
|
475
|
+
if (!allowedTools.has(tool) && !allowedTools.has(capability)) {
|
|
411
476
|
const denial: AgentDenial = {
|
|
412
477
|
agent: agentLabel,
|
|
413
478
|
tool,
|
|
@@ -421,7 +486,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
421
486
|
throw new Error(`ai: model requested unknown tool "${tool}"`);
|
|
422
487
|
}
|
|
423
488
|
|
|
424
|
-
const requiredGates = options.gatesForFlow?.(
|
|
489
|
+
const requiredGates = options.gatesForFlow?.(capability) ?? [];
|
|
425
490
|
if (requiredGates.length > 0 && options.gates) {
|
|
426
491
|
const ctx: GatePolicyContext = {
|
|
427
492
|
auth: auth ?? { userId: null, scopes: new Set() },
|
|
@@ -460,8 +525,9 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
460
525
|
return { error: denial.reason, denied: true };
|
|
461
526
|
}
|
|
462
527
|
|
|
463
|
-
const
|
|
464
|
-
|
|
528
|
+
const invokeSignal = signal ?? currentAbortSignal();
|
|
529
|
+
const output = await withAbortSignal(invokeSignal, () => invoke(capability, args));
|
|
530
|
+
trail.push({ tool: capability, status: "ok", effects, at: now() });
|
|
465
531
|
return output;
|
|
466
532
|
}
|
|
467
533
|
|
|
@@ -491,8 +557,12 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
491
557
|
readonly outputTokens?: number;
|
|
492
558
|
}> {
|
|
493
559
|
const messages = [...opts.messages];
|
|
494
|
-
const defs = toolDefsFor(opts.tools);
|
|
560
|
+
const defs = await toolDefsFor(opts.tools);
|
|
495
561
|
const allowed = new Set(opts.tools);
|
|
562
|
+
for (const name of opts.tools) {
|
|
563
|
+
const mcp = parseMcpToolRef(name);
|
|
564
|
+
if (mcp) allowed.add(mcpModelToolName(mcp.server, mcp.tool));
|
|
565
|
+
}
|
|
496
566
|
const trail: AgentToolStep[] = [];
|
|
497
567
|
const runDenials: AgentDenial[] = [];
|
|
498
568
|
let steps = 0;
|
|
@@ -551,6 +621,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
551
621
|
meta: opts.meta,
|
|
552
622
|
trail,
|
|
553
623
|
runDenials,
|
|
624
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
554
625
|
});
|
|
555
626
|
lastToolResult = toolResult;
|
|
556
627
|
messages.push({
|
|
@@ -594,7 +665,10 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
594
665
|
const version = decl.version;
|
|
595
666
|
const started = now();
|
|
596
667
|
const tools = opts?.tools ?? [];
|
|
597
|
-
const signal = mergeAskAbortSignal(
|
|
668
|
+
const signal = mergeAskAbortSignal(
|
|
669
|
+
resolveTimeoutMs(opts?.timeout ?? decl.timeout),
|
|
670
|
+
currentAbortSignal(),
|
|
671
|
+
);
|
|
598
672
|
|
|
599
673
|
// Replay from journal when input matches (nondeterministic contract)
|
|
600
674
|
if (journalingForced && tools.length === 0) {
|
|
@@ -628,6 +702,28 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
628
702
|
});
|
|
629
703
|
};
|
|
630
704
|
|
|
705
|
+
const assertAskBudget = (spent: number): void => {
|
|
706
|
+
const cap = decl.budget?.maxCostPerCall;
|
|
707
|
+
if (cap === undefined || spent <= cap) return;
|
|
708
|
+
const message = `ai: prompt "${prompt}" exceeded maxCostPerCall ${cap}`;
|
|
709
|
+
if (journalingForced) {
|
|
710
|
+
pushJournal({
|
|
711
|
+
prompt,
|
|
712
|
+
...(version !== undefined ? { version } : {}),
|
|
713
|
+
input,
|
|
714
|
+
output: { error: message },
|
|
715
|
+
attempts: [...attempts],
|
|
716
|
+
outcome: "budget_exceeded",
|
|
717
|
+
cost: spent,
|
|
718
|
+
latencyMs: Math.max(0, now() - started),
|
|
719
|
+
at: now(),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const err = new Error(message);
|
|
723
|
+
err.name = "AiBudgetExceededError";
|
|
724
|
+
throw err;
|
|
725
|
+
};
|
|
726
|
+
|
|
631
727
|
for (const modelName of via) {
|
|
632
728
|
let sameModelTries = 0;
|
|
633
729
|
let advance = true;
|
|
@@ -656,6 +752,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
656
752
|
attemptCost = loop.cost;
|
|
657
753
|
totalCost += attemptCost;
|
|
658
754
|
addUsageTokens(totalTokens, loop);
|
|
755
|
+
assertAskBudget(totalCost);
|
|
659
756
|
if (loop.denials.length > 0 && loop.trail.every((t) => t.status === "denied")) {
|
|
660
757
|
throw new Error(
|
|
661
758
|
`ai: all tool calls denied for prompt "${prompt}": ${loop.denials[0]?.reason}`,
|
|
@@ -671,6 +768,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
671
768
|
attemptCost = result.usage?.cost ?? 0;
|
|
672
769
|
totalCost += attemptCost;
|
|
673
770
|
addUsageTokens(totalTokens, result.usage);
|
|
771
|
+
assertAskBudget(totalCost);
|
|
674
772
|
// Prefer assistant text — `raw` is often the transport envelope
|
|
675
773
|
// (OpenAI chat.completion object), which must not shadow the content.
|
|
676
774
|
raw =
|
|
@@ -815,7 +913,14 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
815
913
|
auth: runOpts.auth,
|
|
816
914
|
operator: runOpts.operator,
|
|
817
915
|
meta: runOpts.meta,
|
|
916
|
+
signal: currentAbortSignal(),
|
|
818
917
|
});
|
|
918
|
+
const runCap = decl.budget?.maxCostPerRun;
|
|
919
|
+
if (runCap !== undefined && loop.cost > runCap) {
|
|
920
|
+
const err = new Error(`ai: agent "${agent}" exceeded maxCostPerRun ${runCap}`);
|
|
921
|
+
err.name = "AiBudgetExceededError";
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
819
924
|
|
|
820
925
|
const record: AgentRunRecord = {
|
|
821
926
|
id: `agent-run-${++runSeq}`,
|
|
@@ -842,21 +947,46 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
842
947
|
},
|
|
843
948
|
|
|
844
949
|
async *stream(model, streamOpts) {
|
|
845
|
-
const
|
|
846
|
-
if (!client.stream) {
|
|
847
|
-
throw new Error(`ai: model "${model}" (driver ${client.driverId}) does not support stream`);
|
|
848
|
-
}
|
|
950
|
+
const via = [model, ...(streamOpts?.via ?? []).filter((name) => name !== model)];
|
|
849
951
|
const content =
|
|
850
952
|
streamOpts?.data !== undefined
|
|
851
953
|
? promptContentFromInput(streamOpts.data)
|
|
852
954
|
: promptContentFromInput(streamOpts?.prompt ?? "");
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
955
|
+
let lastError: unknown;
|
|
956
|
+
for (const modelName of via) {
|
|
957
|
+
let yielded = false;
|
|
958
|
+
try {
|
|
959
|
+
const client = await clientFor(modelName);
|
|
960
|
+
if (!client.stream) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`ai: model "${modelName}" (driver ${client.driverId}) does not support stream`,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
for await (const chunk of client.stream({
|
|
966
|
+
model: wireModel(modelName, client),
|
|
967
|
+
messages: [{ role: "user", content }],
|
|
968
|
+
signal: streamOpts?.signal,
|
|
969
|
+
})) {
|
|
970
|
+
if (chunk.text) {
|
|
971
|
+
yielded = true;
|
|
972
|
+
yield chunk.text;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
} catch (err) {
|
|
977
|
+
lastError = err;
|
|
978
|
+
if (yielded || !isRetryableAiError(err)) {
|
|
979
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
859
982
|
}
|
|
983
|
+
throw lastError instanceof Error
|
|
984
|
+
? lastError
|
|
985
|
+
: new Error(`ai: all models failed to stream: ${String(lastError)}`);
|
|
986
|
+
},
|
|
987
|
+
|
|
988
|
+
async callMcp(ref, input, signal) {
|
|
989
|
+
return mcpClient.call(ref, input, signal ?? currentAbortSignal());
|
|
860
990
|
},
|
|
861
991
|
|
|
862
992
|
async embed(embedName, id, text) {
|
package/src/elements/ai.test.ts
CHANGED
|
@@ -51,6 +51,21 @@ describe("ai declaration", () => {
|
|
|
51
51
|
});
|
|
52
52
|
expect(embed.into).toBe("kb");
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
test("mcpServer requires allowlist and .tool() NamedRef", () => {
|
|
56
|
+
const github = ai.mcpServer("github", {
|
|
57
|
+
url: "https://mcp.example/github",
|
|
58
|
+
tools: ["create_issue"],
|
|
59
|
+
});
|
|
60
|
+
expect(github.kind).toBe("mcp-server");
|
|
61
|
+
expect(github.tools).toEqual(["create_issue"]);
|
|
62
|
+
expect(github.tool("create_issue")).toEqual({ name: "mcp:github/create_issue" });
|
|
63
|
+
expect(() => github.tool("delete_repo")).toThrow(/not in the allowlist/);
|
|
64
|
+
expect(() => ai.mcpServer("broken", { url: "https://x" } as never)).toThrow(/allowlist/);
|
|
65
|
+
expect(() => ai.mcpServer("both", { url: "https://x", command: "npx", tools: ["a"] })).toThrow(
|
|
66
|
+
/exactly one/,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
54
69
|
});
|
|
55
70
|
|
|
56
71
|
describe("agent gate denial is recorded", () => {
|
|
@@ -576,3 +591,127 @@ describe("ask journal tokens", () => {
|
|
|
576
591
|
expect(entry.cost).toBe(0);
|
|
577
592
|
});
|
|
578
593
|
});
|
|
594
|
+
|
|
595
|
+
describe("ask budget and cancel", () => {
|
|
596
|
+
test("maxCostPerCall stops a successful over-budget ask", async () => {
|
|
597
|
+
const smart = ai.model("smart", { provider: "mock" });
|
|
598
|
+
const prompt = smart.prompt("pricey", { budget: { maxCostPerCall: 0.01 } });
|
|
599
|
+
const runtime = createAiRuntime({
|
|
600
|
+
models: [smart],
|
|
601
|
+
prompts: [prompt],
|
|
602
|
+
clients: {
|
|
603
|
+
smart: {
|
|
604
|
+
driverId: "mock",
|
|
605
|
+
model: "smart",
|
|
606
|
+
async complete() {
|
|
607
|
+
return {
|
|
608
|
+
text: "{}",
|
|
609
|
+
raw: {},
|
|
610
|
+
model: "smart",
|
|
611
|
+
driverId: "mock",
|
|
612
|
+
usage: { cost: 0.5 },
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
await expect(runtime.ask("pricey", {})).rejects.toThrow(/maxCostPerCall/);
|
|
619
|
+
expect(runtime.journal[0]!.outcome).toBe("budget_exceeded");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("disconnect AbortError does not advance via", async () => {
|
|
623
|
+
let localCalls = 0;
|
|
624
|
+
const smart = ai.model("smart", { provider: "mock" });
|
|
625
|
+
const local = ai.model("local", { provider: "mock" });
|
|
626
|
+
const prompt = smart.prompt("cancel-me", { via: ["smart", "local"] });
|
|
627
|
+
const runtime = createAiRuntime({
|
|
628
|
+
models: [smart, local],
|
|
629
|
+
prompts: [prompt],
|
|
630
|
+
clients: {
|
|
631
|
+
smart: {
|
|
632
|
+
driverId: "mock",
|
|
633
|
+
model: "smart",
|
|
634
|
+
async complete() {
|
|
635
|
+
const err = new Error("aborted");
|
|
636
|
+
err.name = "AbortError";
|
|
637
|
+
throw err;
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
local: {
|
|
641
|
+
driverId: "mock",
|
|
642
|
+
model: "local",
|
|
643
|
+
async complete() {
|
|
644
|
+
localCalls++;
|
|
645
|
+
return { text: "{}", raw: {}, model: "local", driverId: "mock" };
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
await expect(runtime.ask("cancel-me", {})).rejects.toThrow(/aborted/);
|
|
651
|
+
expect(localCalls).toBe(0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("driverId opens the matching protocol driver", async () => {
|
|
655
|
+
let opened = "";
|
|
656
|
+
const local = ai.model("local", { driverId: "ollama", model: "llama" });
|
|
657
|
+
const prompt = local.prompt("ping");
|
|
658
|
+
const runtime = createAiRuntime({
|
|
659
|
+
models: [local],
|
|
660
|
+
prompts: [prompt],
|
|
661
|
+
drivers: {
|
|
662
|
+
ollama: {
|
|
663
|
+
id: "ollama",
|
|
664
|
+
async open() {
|
|
665
|
+
opened = "ollama";
|
|
666
|
+
return {
|
|
667
|
+
driverId: "ollama",
|
|
668
|
+
model: "llama",
|
|
669
|
+
async complete() {
|
|
670
|
+
return { text: "{}", raw: {}, model: "llama", driverId: "ollama" };
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
await runtime.ask("ping", {});
|
|
678
|
+
expect(opened).toBe("ollama");
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("stream via", () => {
|
|
683
|
+
test("advances to the next model when the first stream fails before a chunk", async () => {
|
|
684
|
+
const smart = ai.model("smart");
|
|
685
|
+
const local = ai.model("local");
|
|
686
|
+
const runtime = createAiRuntime({
|
|
687
|
+
models: [smart, local],
|
|
688
|
+
clients: {
|
|
689
|
+
smart: {
|
|
690
|
+
driverId: "mock",
|
|
691
|
+
model: "smart",
|
|
692
|
+
async complete() {
|
|
693
|
+
return { text: "", model: "smart", driverId: "mock" };
|
|
694
|
+
},
|
|
695
|
+
async *stream() {
|
|
696
|
+
throw new Error("smart stream down");
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
local: {
|
|
700
|
+
driverId: "mock",
|
|
701
|
+
model: "local",
|
|
702
|
+
async complete() {
|
|
703
|
+
return { text: "", model: "local", driverId: "mock" };
|
|
704
|
+
},
|
|
705
|
+
async *stream() {
|
|
706
|
+
yield { text: "ok" };
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
const parts: string[] = [];
|
|
712
|
+
for await (const c of runtime.stream("smart", { prompt: "hi", via: ["local"] })) {
|
|
713
|
+
parts.push(c);
|
|
714
|
+
}
|
|
715
|
+
expect(parts.join("")).toBe("ok");
|
|
716
|
+
});
|
|
717
|
+
});
|
package/src/elements/ai.ts
CHANGED
|
@@ -19,6 +19,10 @@ export type {
|
|
|
19
19
|
AiBudgetDecl,
|
|
20
20
|
AiEmbedDecl,
|
|
21
21
|
AiEmbedOptions,
|
|
22
|
+
AiMcpServerAuthOptions,
|
|
23
|
+
AiMcpServerDecl,
|
|
24
|
+
AiMcpServerOptions,
|
|
25
|
+
AiMcpToolRef,
|
|
22
26
|
AiModelDecl,
|
|
23
27
|
AiModelOptions,
|
|
24
28
|
AiPromptDecl,
|
|
@@ -58,6 +62,18 @@ export type {
|
|
|
58
62
|
CreateAiRuntimeOptions,
|
|
59
63
|
} from "./ai/runtime.ts";
|
|
60
64
|
|
|
65
|
+
export { createMcpClient } from "./ai/mcp-client.ts";
|
|
66
|
+
export type { CreateMcpClientOptions, McpClient, McpResolvedTool } from "./ai/mcp-client.ts";
|
|
67
|
+
export { createMockMcpTransport } from "./ai/mcp-mock.ts";
|
|
68
|
+
export type { CreateMockMcpTransportOptions, MockMcpTool } from "./ai/mcp-mock.ts";
|
|
69
|
+
export {
|
|
70
|
+
MCP_CLIENT_PROTOCOL_VERSION,
|
|
71
|
+
MCP_LEGACY_PROTOCOL_VERSION,
|
|
72
|
+
mcpClientMeta,
|
|
73
|
+
parseJsonRpcResponse,
|
|
74
|
+
} from "./ai/mcp-protocol.ts";
|
|
75
|
+
export type { McpListedTool, McpProtocolEra } from "./ai/mcp-protocol.ts";
|
|
76
|
+
|
|
61
77
|
export { AI_RATE_PRESETS, aiRateGate, createAiRateGates } from "./ai/rate.ts";
|
|
62
78
|
export type { AiRatePreset } from "./ai/rate.ts";
|
|
63
79
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* the effective state from the Store (console §5), never the code directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { clockRegistry } from "../../kernel/element-registries.ts";
|
|
9
|
+
|
|
8
10
|
/** Options for {@link clock}. */
|
|
9
11
|
export interface ClockOptions {
|
|
10
12
|
/** Cron expression (`m h dom mon dow`). */
|
|
@@ -40,6 +42,27 @@ export interface ClockDecl {
|
|
|
40
42
|
readonly description?: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* `clock()` pushes into the shared {@link clockRegistry}
|
|
47
|
+
* (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
|
|
48
|
+
* `clocks` with zero explicit array — mirrors the {@link on} trigger-drain
|
|
49
|
+
* registry (`src/kernel/on.ts`).
|
|
50
|
+
*
|
|
51
|
+
* Snapshot of every clock declared since the last reset.
|
|
52
|
+
*/
|
|
53
|
+
export function listClocks(): readonly ClockDecl[] {
|
|
54
|
+
return clockRegistry.slice();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clear the clock registry (tests / fresh app adopt).
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export function resetClocks(): void {
|
|
63
|
+
clockRegistry.length = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
/**
|
|
44
67
|
* Declare a named clock / cron schedule.
|
|
45
68
|
*
|
|
@@ -50,7 +73,7 @@ export function clock(name: string, options: ClockOptions = {}): ClockDecl {
|
|
|
50
73
|
if (!options.cron && !options.every) {
|
|
51
74
|
throw new TypeError(`clock("${name}"): require cron or every`);
|
|
52
75
|
}
|
|
53
|
-
|
|
76
|
+
const decl: ClockDecl = {
|
|
54
77
|
name,
|
|
55
78
|
cron: options.cron,
|
|
56
79
|
every: options.every,
|
|
@@ -58,4 +81,6 @@ export function clock(name: string, options: ClockOptions = {}): ClockDecl {
|
|
|
58
81
|
overridable: options.overridable ?? false,
|
|
59
82
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
60
83
|
};
|
|
84
|
+
clockRegistry.push(decl);
|
|
85
|
+
return decl;
|
|
61
86
|
}
|
|
@@ -100,4 +100,47 @@ describe("nextOccurrences", () => {
|
|
|
100
100
|
expect(fires.length).toBeGreaterThanOrEqual(1);
|
|
101
101
|
expect(fires.every((t) => t >= from && t < until)).toBe(true);
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
test("*/15 and @hourly via Bun.cron.parse", () => {
|
|
105
|
+
const from = Date.UTC(2024, 0, 1, 0, 0, 0);
|
|
106
|
+
const until = from + 60 * 60_000;
|
|
107
|
+
const quarters = nextOccurrences(
|
|
108
|
+
row({
|
|
109
|
+
name: "quarters",
|
|
110
|
+
effectiveCron: "*/15 * * * *",
|
|
111
|
+
timezone: "UTC",
|
|
112
|
+
}),
|
|
113
|
+
from,
|
|
114
|
+
until,
|
|
115
|
+
);
|
|
116
|
+
expect(quarters).toEqual([from, from + 15 * 60_000, from + 30 * 60_000, from + 45 * 60_000]);
|
|
117
|
+
|
|
118
|
+
const hours = nextOccurrences(
|
|
119
|
+
row({
|
|
120
|
+
name: "hourly",
|
|
121
|
+
effectiveCron: "@hourly",
|
|
122
|
+
timezone: "UTC",
|
|
123
|
+
}),
|
|
124
|
+
from,
|
|
125
|
+
from + 3 * 3_600_000,
|
|
126
|
+
);
|
|
127
|
+
expect(hours).toEqual([from, from + 3_600_000, from + 2 * 3_600_000]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("cron timezone is the declared IANA zone", () => {
|
|
131
|
+
const from = Date.UTC(2024, 0, 1, 0, 0, 0);
|
|
132
|
+
const until = from + 36 * 3_600_000;
|
|
133
|
+
const fires = nextOccurrences(
|
|
134
|
+
row({
|
|
135
|
+
name: "ny",
|
|
136
|
+
effectiveCron: "0 9 * * *",
|
|
137
|
+
timezone: "America/New_York",
|
|
138
|
+
}),
|
|
139
|
+
from,
|
|
140
|
+
until,
|
|
141
|
+
);
|
|
142
|
+
expect(fires).toHaveLength(1);
|
|
143
|
+
// 09:00 America/New_York on 2024-01-01 is 14:00 UTC (EST).
|
|
144
|
+
expect(fires[0]).toBe(Date.UTC(2024, 0, 1, 14, 0, 0));
|
|
145
|
+
});
|
|
103
146
|
});
|