okengine 0.22.0 → 0.23.1
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/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Emit still enrols in a transactional outbox (semantics never regress).
|
|
5
5
|
* After commit, a relay pushes to Redis Streams (`once`) or pub/sub
|
|
6
|
-
* (`broadcast` / `live`).
|
|
7
|
-
*
|
|
6
|
+
* (`broadcast` / `live`). With `compete: true`, `once` delivery is
|
|
7
|
+
* `XREADGROUP` / `XACK` (pending reclaim on the next drain). Broadcast
|
|
8
|
+
* and live stay on the outbox plus pub/sub.
|
|
8
9
|
*
|
|
9
10
|
* Production bind: {@link createBunSignalRedisClient} (typed `publish` /
|
|
10
11
|
* `subscribe` / `xadd` / `xreadgroup` / `xack` / `xgroup` on Bun ≥1.4).
|
|
@@ -12,8 +13,12 @@
|
|
|
12
13
|
|
|
13
14
|
import { createSignalEngine } from "./signal-engine.ts";
|
|
14
15
|
import type {
|
|
16
|
+
DeadLetter,
|
|
15
17
|
SignalBus,
|
|
16
18
|
SignalDriver,
|
|
19
|
+
SignalFailureReason,
|
|
20
|
+
SignalHandler,
|
|
21
|
+
SignalMessage,
|
|
17
22
|
SignalOpenOptions,
|
|
18
23
|
SignalRedisClientLike,
|
|
19
24
|
} from "./signal-types.ts";
|
|
@@ -68,6 +73,32 @@ export function createBunSignalRedisClient(url?: string): SignalRedisClientLike
|
|
|
68
73
|
}
|
|
69
74
|
},
|
|
70
75
|
async xreadgroup(group, consumer, key, count) {
|
|
76
|
+
try {
|
|
77
|
+
await redis.send("XAUTOCLAIM", [
|
|
78
|
+
key,
|
|
79
|
+
group,
|
|
80
|
+
consumer,
|
|
81
|
+
"5000",
|
|
82
|
+
"0-0",
|
|
83
|
+
"COUNT",
|
|
84
|
+
String(count),
|
|
85
|
+
]);
|
|
86
|
+
} catch {
|
|
87
|
+
/* Idle reclaim is best-effort; same-consumer pending still returns below. */
|
|
88
|
+
}
|
|
89
|
+
const pending = parseXreadgroupReply(
|
|
90
|
+
await typedStreams(redis).xreadgroup(
|
|
91
|
+
"GROUP",
|
|
92
|
+
group,
|
|
93
|
+
consumer,
|
|
94
|
+
"COUNT",
|
|
95
|
+
count,
|
|
96
|
+
"STREAMS",
|
|
97
|
+
key,
|
|
98
|
+
"0",
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
if (pending.length > 0) return pending;
|
|
71
102
|
const reply = await typedStreams(redis).xreadgroup(
|
|
72
103
|
"GROUP",
|
|
73
104
|
group,
|
|
@@ -138,6 +169,10 @@ export function createSignalRedisFake(): SignalRedisClientLike & {
|
|
|
138
169
|
const streams = new Map<string, Array<{ id: string; fields: Record<string, string> }>>();
|
|
139
170
|
const groups = new Map<string, Set<string>>();
|
|
140
171
|
const claimed = new Map<string, Set<string>>();
|
|
172
|
+
const pending = new Map<
|
|
173
|
+
string,
|
|
174
|
+
Array<{ id: string; fields: Record<string, string>; consumer: string }>
|
|
175
|
+
>();
|
|
141
176
|
const subs = new Map<string, Set<(message: string) => void>>();
|
|
142
177
|
const published: Array<{ channel: string; message: string }> = [];
|
|
143
178
|
let seq = 0;
|
|
@@ -160,6 +195,7 @@ export function createSignalRedisFake(): SignalRedisClientLike & {
|
|
|
160
195
|
const gkey = `${key}::${group}`;
|
|
161
196
|
if (!groups.has(gkey)) groups.set(gkey, new Set());
|
|
162
197
|
if (!claimed.has(gkey)) claimed.set(gkey, new Set());
|
|
198
|
+
if (!pending.has(gkey)) pending.set(gkey, []);
|
|
163
199
|
},
|
|
164
200
|
async xreadgroup(group, consumer, key, count) {
|
|
165
201
|
const gkey = `${key}::${group}`;
|
|
@@ -167,21 +203,36 @@ export function createSignalRedisFake(): SignalRedisClientLike & {
|
|
|
167
203
|
await this.xgroupCreate(key, group, "0", { mkstream: true });
|
|
168
204
|
}
|
|
169
205
|
const seen = claimed.get(gkey)!;
|
|
206
|
+
const held = pending.get(gkey)!;
|
|
170
207
|
const list = streams.get(key) ?? [];
|
|
171
208
|
const out: Array<{ id: string; fields: Record<string, string> }> = [];
|
|
209
|
+
for (const entry of held) {
|
|
210
|
+
if (entry.consumer !== consumer) continue;
|
|
211
|
+
out.push({ id: entry.id, fields: { ...entry.fields } });
|
|
212
|
+
if (out.length >= count) return out;
|
|
213
|
+
}
|
|
172
214
|
for (const entry of list) {
|
|
173
215
|
if (seen.has(entry.id)) continue;
|
|
174
216
|
seen.add(entry.id);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
217
|
+
held.push({ id: entry.id, fields: { ...entry.fields }, consumer });
|
|
218
|
+
out.push({ id: entry.id, fields: { ...entry.fields } });
|
|
219
|
+
if (out.length >= count) return out;
|
|
220
|
+
}
|
|
221
|
+
for (const entry of held) {
|
|
222
|
+
if (entry.consumer === consumer) continue;
|
|
223
|
+
entry.consumer = consumer;
|
|
224
|
+
out.push({ id: entry.id, fields: { ...entry.fields } });
|
|
179
225
|
if (out.length >= count) break;
|
|
180
226
|
}
|
|
181
227
|
return out;
|
|
182
228
|
},
|
|
183
|
-
async xack() {
|
|
184
|
-
|
|
229
|
+
async xack(key, group, id) {
|
|
230
|
+
const gkey = `${key}::${group}`;
|
|
231
|
+
const held = pending.get(gkey);
|
|
232
|
+
if (!held) return 0;
|
|
233
|
+
const next = held.filter((entry) => entry.id !== id);
|
|
234
|
+
pending.set(gkey, next);
|
|
235
|
+
return next.length === held.length ? 0 : 1;
|
|
185
236
|
},
|
|
186
237
|
async publish(channel, message) {
|
|
187
238
|
published.push({ channel, message });
|
|
@@ -214,6 +265,93 @@ export async function openRedisSignal(options: SignalOpenOptions): Promise<Signa
|
|
|
214
265
|
// binds Bun.redis (Streams via send until Bun ships typed xadd).
|
|
215
266
|
const redis = options.redis ?? createBunSignalRedisClient();
|
|
216
267
|
const outbox = await createSignalEngine("redis", options);
|
|
268
|
+
const compete = options.compete === true;
|
|
269
|
+
const onceHandlers = new Map<string, SignalHandler[]>();
|
|
270
|
+
const streamDead: DeadLetter[] = [];
|
|
271
|
+
const attemptById = new Map<string, number>();
|
|
272
|
+
const now = options.now ?? (() => Date.now());
|
|
273
|
+
|
|
274
|
+
if (compete) {
|
|
275
|
+
for (const [name, decl] of options.signals) {
|
|
276
|
+
if (decl.delivery !== "once") continue;
|
|
277
|
+
outbox.subscribe(name, "oke-relay-ack", async () => {});
|
|
278
|
+
onceHandlers.set(name, []);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function streamMessage(
|
|
283
|
+
name: string,
|
|
284
|
+
id: string,
|
|
285
|
+
payload: unknown,
|
|
286
|
+
attempt: number,
|
|
287
|
+
): SignalMessage {
|
|
288
|
+
const at = now();
|
|
289
|
+
return {
|
|
290
|
+
id,
|
|
291
|
+
signal: name,
|
|
292
|
+
payload,
|
|
293
|
+
delivery: "once",
|
|
294
|
+
attempts: attempt,
|
|
295
|
+
failures: [],
|
|
296
|
+
createdAt: at,
|
|
297
|
+
availableAt: at,
|
|
298
|
+
status: "inflight",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function drainOnceStreams(): Promise<void> {
|
|
303
|
+
const group = "oke";
|
|
304
|
+
const consumer = options.consumerId ?? "local";
|
|
305
|
+
for (const [name, decl] of options.signals) {
|
|
306
|
+
if (decl.delivery !== "once") continue;
|
|
307
|
+
const key = `oke:signal:${name}`;
|
|
308
|
+
await redis.xgroupCreate(key, group, "0", { mkstream: true });
|
|
309
|
+
const rows = await redis.xreadgroup(group, consumer, key, 32);
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
const handler = onceHandlers.get(name)?.[0];
|
|
312
|
+
let payload: unknown = null;
|
|
313
|
+
try {
|
|
314
|
+
payload = JSON.parse(row.fields.payload ?? "null");
|
|
315
|
+
} catch {
|
|
316
|
+
payload = null;
|
|
317
|
+
}
|
|
318
|
+
const attempt = (attemptById.get(row.id) ?? 0) + 1;
|
|
319
|
+
attemptById.set(row.id, attempt);
|
|
320
|
+
if (!handler) continue;
|
|
321
|
+
try {
|
|
322
|
+
await handler(streamMessage(name, row.id, payload, attempt));
|
|
323
|
+
await redis.xack(key, group, row.id);
|
|
324
|
+
attemptById.delete(row.id);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const retries = decl.retries ?? 3;
|
|
327
|
+
if (attempt > retries) {
|
|
328
|
+
const failure: SignalFailureReason = {
|
|
329
|
+
code: "handler_error",
|
|
330
|
+
message: err instanceof Error ? err.message : String(err),
|
|
331
|
+
at: now(),
|
|
332
|
+
attempt,
|
|
333
|
+
};
|
|
334
|
+
if (decl.deadLetter) {
|
|
335
|
+
const at = now();
|
|
336
|
+
streamDead.push({
|
|
337
|
+
id: row.id,
|
|
338
|
+
signal: name,
|
|
339
|
+
payload,
|
|
340
|
+
delivery: "once",
|
|
341
|
+
attempts: attempt,
|
|
342
|
+
failures: [failure],
|
|
343
|
+
createdAt: at,
|
|
344
|
+
availableAt: at,
|
|
345
|
+
status: "dead",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
await redis.xack(key, group, row.id);
|
|
349
|
+
attemptById.delete(row.id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
217
355
|
|
|
218
356
|
async function relayToRedis(signal: string, payload: unknown): Promise<void> {
|
|
219
357
|
const decl = options.signals.get(signal);
|
|
@@ -252,11 +390,29 @@ export async function openRedisSignal(options: SignalOpenOptions): Promise<Signa
|
|
|
252
390
|
rollback: () => tx.rollback(),
|
|
253
391
|
};
|
|
254
392
|
},
|
|
255
|
-
subscribe
|
|
393
|
+
subscribe(signal, subscriberId, handler) {
|
|
394
|
+
if (compete && options.signals.get(signal)?.delivery === "once") {
|
|
395
|
+
const list = onceHandlers.get(signal) ?? [];
|
|
396
|
+
list.push(handler);
|
|
397
|
+
onceHandlers.set(signal, list);
|
|
398
|
+
return Promise.resolve(() => {
|
|
399
|
+
const next = (onceHandlers.get(signal) ?? []).filter((h) => h !== handler);
|
|
400
|
+
onceHandlers.set(signal, next);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return outbox.subscribe(signal, subscriberId, handler);
|
|
404
|
+
},
|
|
256
405
|
live: (signal, opts) => outbox.live(signal, opts),
|
|
257
406
|
checkLiveResume: (signal, afterId) => outbox.checkLiveResume(signal, afterId),
|
|
258
|
-
drain
|
|
259
|
-
|
|
407
|
+
async drain() {
|
|
408
|
+
if (compete) await drainOnceStreams();
|
|
409
|
+
await outbox.drain();
|
|
410
|
+
},
|
|
411
|
+
async deadLetters(s) {
|
|
412
|
+
const local = streamDead.filter((entry) => entry.signal === s);
|
|
413
|
+
const rest = await outbox.deadLetters(s);
|
|
414
|
+
return [...rest, ...local];
|
|
415
|
+
},
|
|
260
416
|
inspect: (s) => outbox.inspect(s),
|
|
261
417
|
replay: (opts) => outbox.replay(opts),
|
|
262
418
|
discard: (opts) => outbox.discard(opts),
|
|
@@ -265,6 +265,18 @@ export interface SignalOpenOptions {
|
|
|
265
265
|
readonly nats?: SignalNatsClientLike;
|
|
266
266
|
/** Outbox SQL for redis/nats relay (defaults to in-process). */
|
|
267
267
|
readonly outboxSql?: unknown;
|
|
268
|
+
/**
|
|
269
|
+
* When the SQL client has no `LISTEN`, poll `drain` on this interval (ms).
|
|
270
|
+
* `0` or omitted with a `listen` client uses notifications only.
|
|
271
|
+
*/
|
|
272
|
+
readonly pollMs?: number;
|
|
273
|
+
/**
|
|
274
|
+
* Redis `once` consume via a consumer group (`xreadgroup` / `xack`)
|
|
275
|
+
* instead of the process-local outbox. Boot sets this for `drivers.signal: redis`.
|
|
276
|
+
*/
|
|
277
|
+
readonly compete?: boolean;
|
|
278
|
+
/** Consumer name inside the redis group. Defaults to `"local"`. */
|
|
279
|
+
readonly consumerId?: string;
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installed agent event log.
|
|
3
|
+
*
|
|
4
|
+
* The follow route and the AI runtime must share one slot. The log
|
|
5
|
+
* implementation stays in `run-events.ts` and loads on first use; this file
|
|
6
|
+
* is the only static edge so a `lazyRequire` copy cannot hide the log.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentEventLog } from "./run-events.ts";
|
|
10
|
+
|
|
11
|
+
let activeLog: AgentEventLog | undefined;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Install the log the follow route reads. Boot and tests call this.
|
|
15
|
+
*
|
|
16
|
+
* @param log - Active log
|
|
17
|
+
*/
|
|
18
|
+
export function setAgentEventLog(log: AgentEventLog | undefined): void {
|
|
19
|
+
activeLog = log;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Log installed by the running AI runtime.
|
|
24
|
+
*/
|
|
25
|
+
export function getAgentEventLog(): AgentEventLog | undefined {
|
|
26
|
+
return activeLog;
|
|
27
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in HTTP approve / deny for a parked agent tool.
|
|
3
|
+
*
|
|
4
|
+
* The route is public so the request can arrive. The tool's own gate is
|
|
5
|
+
* checked inside `fx.agent`. `Idempotency-Key` applies because the flow writes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { flow } from "../../kernel/flow.ts";
|
|
9
|
+
import type { Fx } from "../../kernel/fx.ts";
|
|
10
|
+
import { lazyRequire } from "../../kernel/lazy-require.ts";
|
|
11
|
+
import type { Binding } from "../../kernel/on.ts";
|
|
12
|
+
import { http } from "../../kernel/triggers.ts";
|
|
13
|
+
import type { GateRuntime } from "../gate/runtime.ts";
|
|
14
|
+
import type { StandardSchemaV1 } from "../../validation/standard-schema.ts";
|
|
15
|
+
import { journalLeaseBusyResponse } from "./approval.ts";
|
|
16
|
+
import { getAgentEventLog } from "./agent-event-slot.ts";
|
|
17
|
+
import type { AgentEventLog } from "./run-events.ts";
|
|
18
|
+
|
|
19
|
+
/** SSE frame helpers live on `fx.ts`. Load them on the first follow frame. */
|
|
20
|
+
function loadSseFrames(): typeof import("../../kernel/fx.ts") {
|
|
21
|
+
return lazyRequire(`${import.meta.dir}/../../kernel`, ["f", "x"].join(""));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Body the approve route accepts. `args` replaces the tool input. */
|
|
25
|
+
const approveIn: StandardSchemaV1<{ id: string; args?: unknown }> = {
|
|
26
|
+
"~standard": {
|
|
27
|
+
version: 1,
|
|
28
|
+
vendor: "okengine",
|
|
29
|
+
validate(value) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
31
|
+
return { issues: [{ message: "expected object", path: [] }] };
|
|
32
|
+
}
|
|
33
|
+
const body = value as { id?: unknown; args?: unknown };
|
|
34
|
+
if (typeof body.id !== "string" || body.id.length === 0) {
|
|
35
|
+
return { issues: [{ message: "id is required", path: ["id"] }] };
|
|
36
|
+
}
|
|
37
|
+
return { value: { id: body.id, ...(body.args !== undefined ? { args: body.args } : {}) } };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Body the deny route accepts. */
|
|
43
|
+
const denyIn: StandardSchemaV1<{ id: string; reason?: string }> = {
|
|
44
|
+
"~standard": {
|
|
45
|
+
version: 1,
|
|
46
|
+
vendor: "okengine",
|
|
47
|
+
validate(value) {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
49
|
+
return { issues: [{ message: "expected object", path: [] }] };
|
|
50
|
+
}
|
|
51
|
+
const body = value as { id?: unknown; reason?: unknown };
|
|
52
|
+
if (typeof body.id !== "string" || body.id.length === 0) {
|
|
53
|
+
return { issues: [{ message: "id is required", path: ["id"] }] };
|
|
54
|
+
}
|
|
55
|
+
if (body.reason !== undefined && typeof body.reason !== "string") {
|
|
56
|
+
return { issues: [{ message: "reason must be a string", path: ["reason"] }] };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
value: {
|
|
60
|
+
id: body.id,
|
|
61
|
+
...(typeof body.reason === "string" ? { reason: body.reason } : {}),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Map a resolution to the HTTP failure the rest of the kernel already speaks.
|
|
70
|
+
*
|
|
71
|
+
* @param fx - Flow context
|
|
72
|
+
* @param result - Approval resolution
|
|
73
|
+
*/
|
|
74
|
+
function approvalHttpResult(
|
|
75
|
+
fx: Fx,
|
|
76
|
+
result:
|
|
77
|
+
| { readonly ok: true }
|
|
78
|
+
| { readonly ok: false; readonly status: 403 | 404 }
|
|
79
|
+
| { readonly ok: false; readonly status: 409; readonly reason: "resolved" }
|
|
80
|
+
| {
|
|
81
|
+
readonly ok: false;
|
|
82
|
+
readonly status: 409;
|
|
83
|
+
readonly reason: "lease";
|
|
84
|
+
readonly retryAfterSeconds: number;
|
|
85
|
+
},
|
|
86
|
+
): unknown {
|
|
87
|
+
if (result.ok) return { ok: true };
|
|
88
|
+
if (result.status === 409 && result.reason === "lease") {
|
|
89
|
+
return journalLeaseBusyResponse(result.retryAfterSeconds);
|
|
90
|
+
}
|
|
91
|
+
if (result.status === 409) return fx.fail.conflict();
|
|
92
|
+
if (result.status === 403) return fx.fail.forbidden();
|
|
93
|
+
return fx.fail.notFound();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register the approve and deny routes on an app.
|
|
98
|
+
*
|
|
99
|
+
* @param adopt - App binding adopter
|
|
100
|
+
*/
|
|
101
|
+
export function bindAgentApprovalFlows(adopt: (binding: Binding) => void): void {
|
|
102
|
+
adopt({
|
|
103
|
+
trigger: http.post("/agent/approvals/approve", { in: approveIn }).public(),
|
|
104
|
+
flow: flow("oke.agent.approve", {
|
|
105
|
+
effects: { writes: ["journal:runs"] },
|
|
106
|
+
do: async (input, fx) => {
|
|
107
|
+
const body = input as { id: string; args?: unknown };
|
|
108
|
+
const result = await fx.agent.approve(body.id, { args: body.args });
|
|
109
|
+
return approvalHttpResult(fx, result);
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
adopt({
|
|
114
|
+
trigger: http.post("/agent/approvals/deny", { in: denyIn }).public(),
|
|
115
|
+
flow: flow("oke.agent.deny", {
|
|
116
|
+
effects: { writes: ["journal:runs"] },
|
|
117
|
+
do: async (input, fx) => {
|
|
118
|
+
const body = input as { id: string; reason?: string };
|
|
119
|
+
const result = await fx.agent.deny(body.id, { reason: body.reason });
|
|
120
|
+
return approvalHttpResult(fx, result);
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
adopt({
|
|
125
|
+
trigger: http.get("/agent/runs/:runId/events").public(),
|
|
126
|
+
flow: flow("oke.agent.follow", {
|
|
127
|
+
do: async (input, fx) => followAgentRun(input, fx),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let followGates: GateRuntime | undefined;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Gate runtime the follow route uses. Boot installs the app's gates.
|
|
136
|
+
*
|
|
137
|
+
* @param gates - Runtime that checked the calling Flow
|
|
138
|
+
*/
|
|
139
|
+
export function setAgentFollowGates(gates: GateRuntime | undefined): void {
|
|
140
|
+
followGates = gates;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function followAgentRun(input: unknown, fx: Fx): Promise<unknown> {
|
|
144
|
+
const body = (input ?? {}) as { runId?: unknown };
|
|
145
|
+
const runId = typeof body.runId === "string" ? body.runId : "";
|
|
146
|
+
const log = getAgentEventLog();
|
|
147
|
+
if (!log || !runId) return fx.fail.notFound();
|
|
148
|
+
const header = await log.header(runId);
|
|
149
|
+
if (!header) return fx.fail.notFound();
|
|
150
|
+
if ((header.tenant ?? null) !== (fx.tenant.id ?? null)) return fx.fail.notFound();
|
|
151
|
+
const followerIsOperator = fx.operator.id !== null;
|
|
152
|
+
const sameUser = (header.userId ?? null) === (fx.auth.userId ?? null);
|
|
153
|
+
const sameOperator = (header.operatorId ?? null) === (fx.operator.id ?? null);
|
|
154
|
+
const samePrincipal =
|
|
155
|
+
(header.userId === null && header.operatorId === null && sameUser && sameOperator) ||
|
|
156
|
+
(header.userId !== null && sameUser) ||
|
|
157
|
+
(header.operatorId !== null && sameOperator);
|
|
158
|
+
if (!followerIsOperator && !samePrincipal) return fx.fail.forbidden();
|
|
159
|
+
if (header.gates.length > 0) {
|
|
160
|
+
if (!followGates) return fx.fail.forbidden();
|
|
161
|
+
const checks = await followGates.check(header.gates, {
|
|
162
|
+
auth: fx.auth,
|
|
163
|
+
operator: fx.operator,
|
|
164
|
+
meta: {},
|
|
165
|
+
});
|
|
166
|
+
if (checks.some((check) => check.allowed === false)) return fx.fail.forbidden();
|
|
167
|
+
}
|
|
168
|
+
const after = Number(fx.lastEventId ?? "0");
|
|
169
|
+
const afterSeq = Number.isFinite(after) ? after : 0;
|
|
170
|
+
return fx.json.stream(frames(log, runId, afterSeq, fx.signal));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function* frames(
|
|
174
|
+
log: AgentEventLog,
|
|
175
|
+
runId: string,
|
|
176
|
+
afterSeq: number,
|
|
177
|
+
signal: AbortSignal,
|
|
178
|
+
): AsyncIterable<unknown> {
|
|
179
|
+
const iter = log.subscribe(runId, afterSeq, signal)[Symbol.asyncIterator]();
|
|
180
|
+
let pending = iter.next();
|
|
181
|
+
try {
|
|
182
|
+
for (;;) {
|
|
183
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
184
|
+
const ping = new Promise<"ping">((resolve) => {
|
|
185
|
+
timer = setTimeout(() => resolve("ping"), 15_000);
|
|
186
|
+
});
|
|
187
|
+
const winner = await Promise.race([
|
|
188
|
+
pending.then((row) => ({ kind: "row" as const, row })),
|
|
189
|
+
ping.then((kind) => ({ kind })),
|
|
190
|
+
]);
|
|
191
|
+
if (timer) clearTimeout(timer);
|
|
192
|
+
if (winner.kind === "ping") {
|
|
193
|
+
if (signal.aborted) return;
|
|
194
|
+
yield loadSseFrames().sseComment("keepalive");
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
pending = iter.next();
|
|
198
|
+
if (winner.row.done || winner.row.value === undefined) return;
|
|
199
|
+
const row = winner.row.value;
|
|
200
|
+
yield loadSseFrames().sseFrame(row.event, String(row.seq));
|
|
201
|
+
if (row.event.type === "RUN_ERROR") return;
|
|
202
|
+
if (row.event.type === "RUN_FINISHED" && row.event.outcome?.type !== "interrupt") return;
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
await iter.return?.();
|
|
206
|
+
}
|
|
207
|
+
}
|