okengine 0.21.1 → 0.23.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/AGENTS.md +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.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/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- 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 +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- 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/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/competitor-mention-removal.test.ts +3 -3
- 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/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- 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-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- 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-invoke.test.ts +2 -2
- 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-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.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-CgkKlA56.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-Y6HLjyJ5.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-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.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-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.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-B54v4Yit.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 +15 -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/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.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.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- 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/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- 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/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -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/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- 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 +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- 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/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
package/src/client/transport.ts
CHANGED
|
@@ -20,11 +20,18 @@ import {
|
|
|
20
20
|
toQuery,
|
|
21
21
|
} from "./wire.ts";
|
|
22
22
|
|
|
23
|
-
/** Per-call transport options (binary decode, abort). */
|
|
23
|
+
/** Per-call transport options (binary decode, abort, retry opt-in). */
|
|
24
24
|
export interface TransportCallOptions {
|
|
25
25
|
readonly headers?: ClientHeaders;
|
|
26
26
|
readonly response?: "json" | "blob" | "arrayBuffer";
|
|
27
27
|
readonly signal?: AbortSignal;
|
|
28
|
+
/**
|
|
29
|
+
* Repeat this call under the client `retry` policy even when it sends
|
|
30
|
+
* no idempotency key.
|
|
31
|
+
*/
|
|
32
|
+
readonly retry?: boolean;
|
|
33
|
+
/** Stable key, or `false` to send none. */
|
|
34
|
+
readonly idempotencyKey?: string | false;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
/** Internal transport handle. */
|
|
@@ -63,10 +70,25 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
63
70
|
let delay = delay0;
|
|
64
71
|
const callClientOpts: ClientOptions =
|
|
65
72
|
callOpts.signal !== undefined ? { ...opts, signal: callOpts.signal } : opts;
|
|
73
|
+
const method = (opts.routes?.[key.replace("/", ".")]?.method ?? "POST").toUpperCase();
|
|
74
|
+
const idempotencyKey = resolveIdempotencyKey(method, callOpts.idempotencyKey);
|
|
75
|
+
const allowRetry =
|
|
76
|
+
callOpts.retry === true ||
|
|
77
|
+
method === "GET" ||
|
|
78
|
+
method === "QUERY" ||
|
|
79
|
+
idempotencyKey !== undefined;
|
|
66
80
|
|
|
67
81
|
for (;;) {
|
|
68
82
|
try {
|
|
69
|
-
const res = await once(
|
|
83
|
+
const res = await once(
|
|
84
|
+
base,
|
|
85
|
+
key,
|
|
86
|
+
input,
|
|
87
|
+
callClientOpts,
|
|
88
|
+
fetchFn,
|
|
89
|
+
callOpts.headers,
|
|
90
|
+
idempotencyKey,
|
|
91
|
+
);
|
|
70
92
|
if (
|
|
71
93
|
res.status === 401 &&
|
|
72
94
|
opts.auth &&
|
|
@@ -78,9 +100,23 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
78
100
|
await opts.auth.refresh();
|
|
79
101
|
continue;
|
|
80
102
|
}
|
|
103
|
+
if (res.status === 409 && allowRetry && attempt < retries) {
|
|
104
|
+
const seconds = Number(res.headers.get("retry-after"));
|
|
105
|
+
const wait = seconds > 0 ? seconds * 1000 : delay;
|
|
106
|
+
const structured = await decodeIfEnvelope(res);
|
|
107
|
+
const code = structured?.error?.code;
|
|
108
|
+
if (code === "IdempotencyInProgress" || code === "JournalLeaseBusy") {
|
|
109
|
+
await sleep(wait);
|
|
110
|
+
delay *= backoff;
|
|
111
|
+
attempt += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (structured) return stampReplay(structured, res);
|
|
115
|
+
return transportEnvelope("HTTP 409", 409);
|
|
116
|
+
}
|
|
81
117
|
if (res.status >= 500) {
|
|
82
118
|
const structured = await decodeIfEnvelope(res);
|
|
83
|
-
if (structured) return structured;
|
|
119
|
+
if (structured) return stampReplay(structured, res);
|
|
84
120
|
throw new Error(`HTTP ${res.status}`);
|
|
85
121
|
}
|
|
86
122
|
if (callOpts.response === "blob" || callOpts.response === "arrayBuffer") {
|
|
@@ -89,7 +125,7 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
89
125
|
return decode(res);
|
|
90
126
|
} catch (err) {
|
|
91
127
|
const transient = isTransient(err);
|
|
92
|
-
if (!transient || attempt >= retries) {
|
|
128
|
+
if (!transient || !allowRetry || attempt >= retries) {
|
|
93
129
|
return transportEnvelope(err instanceof Error ? err.message : String(err));
|
|
94
130
|
}
|
|
95
131
|
await sleep(delay);
|
|
@@ -127,10 +163,20 @@ function normalizeCallOpts(
|
|
|
127
163
|
if (Array.isArray(headersOrOpts)) return { headers: headersOrOpts };
|
|
128
164
|
if (
|
|
129
165
|
typeof headersOrOpts === "object" &&
|
|
130
|
-
("response" in headersOrOpts ||
|
|
166
|
+
("response" in headersOrOpts ||
|
|
167
|
+
"signal" in headersOrOpts ||
|
|
168
|
+
"headers" in headersOrOpts ||
|
|
169
|
+
"retry" in headersOrOpts ||
|
|
170
|
+
"idempotencyKey" in headersOrOpts)
|
|
131
171
|
) {
|
|
132
172
|
const o = headersOrOpts as TransportCallOptions;
|
|
133
|
-
if (
|
|
173
|
+
if (
|
|
174
|
+
o.response !== undefined ||
|
|
175
|
+
o.signal !== undefined ||
|
|
176
|
+
o.headers !== undefined ||
|
|
177
|
+
o.retry !== undefined ||
|
|
178
|
+
o.idempotencyKey !== undefined
|
|
179
|
+
) {
|
|
134
180
|
return o;
|
|
135
181
|
}
|
|
136
182
|
}
|
|
@@ -138,7 +184,9 @@ function normalizeCallOpts(
|
|
|
138
184
|
}
|
|
139
185
|
|
|
140
186
|
/**
|
|
141
|
-
* Single HTTP attempt. Throws on network / abort / 5xx
|
|
187
|
+
* Single HTTP attempt. Throws on network / abort / non-envelope 5xx.
|
|
188
|
+
* A client timeout is rethrown as a plain `Error` so the retry loop can
|
|
189
|
+
* repeat it. A caller `AbortSignal` stays an `AbortError` and is not retried.
|
|
142
190
|
*
|
|
143
191
|
* @param base - Origin
|
|
144
192
|
* @param key - `unit/flow`
|
|
@@ -154,6 +202,7 @@ async function once(
|
|
|
154
202
|
opts: ClientOptions,
|
|
155
203
|
fetchFn: ClientFetch,
|
|
156
204
|
callHeaders?: ClientHeaders,
|
|
205
|
+
idempotencyKey?: string,
|
|
157
206
|
): Promise<Response> {
|
|
158
207
|
const route = opts.routes?.[key.replace("/", ".")];
|
|
159
208
|
const { url, method, body } = route
|
|
@@ -163,6 +212,7 @@ async function once(
|
|
|
163
212
|
const headers = new Headers();
|
|
164
213
|
applyHeaderBag(headers, await resolveHeaders(opts));
|
|
165
214
|
applyHeaderBag(headers, callHeaders);
|
|
215
|
+
if (idempotencyKey !== undefined) headers.set("Idempotency-Key", idempotencyKey);
|
|
166
216
|
if (body !== undefined && !headers.has("content-type") && typeof body === "string") {
|
|
167
217
|
headers.set("content-type", "application/json");
|
|
168
218
|
}
|
|
@@ -172,13 +222,20 @@ async function once(
|
|
|
172
222
|
const signal =
|
|
173
223
|
opts.signal && timeout ? AbortSignal.any([opts.signal, timeout]) : (opts.signal ?? timeout);
|
|
174
224
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
225
|
+
try {
|
|
226
|
+
return await fetchFn(url, {
|
|
227
|
+
method,
|
|
228
|
+
headers,
|
|
229
|
+
body: body as RequestInit["body"],
|
|
230
|
+
signal,
|
|
231
|
+
...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
|
|
232
|
+
});
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (isAbortError(err) && timeout?.aborted === true && opts.signal?.aborted !== true) {
|
|
235
|
+
throw new Error(err instanceof Error ? err.message : "timeout");
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
182
239
|
}
|
|
183
240
|
|
|
184
241
|
/**
|
|
@@ -253,12 +310,12 @@ function isRawBody(input: unknown): input is ClientBodyInit {
|
|
|
253
310
|
|
|
254
311
|
async function decode(res: Response): Promise<ClientEnvelope> {
|
|
255
312
|
if (res.status === 204) {
|
|
256
|
-
return { data: undefined, error: null };
|
|
313
|
+
return stampReplay({ data: undefined, error: null }, res);
|
|
257
314
|
}
|
|
258
315
|
|
|
259
316
|
const text = await res.text();
|
|
260
317
|
if (!text) {
|
|
261
|
-
if (res.ok) return { data: undefined, error: null };
|
|
318
|
+
if (res.ok) return stampReplay({ data: undefined, error: null }, res);
|
|
262
319
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
263
320
|
}
|
|
264
321
|
|
|
@@ -270,11 +327,11 @@ async function decode(res: Response): Promise<ClientEnvelope> {
|
|
|
270
327
|
}
|
|
271
328
|
|
|
272
329
|
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
273
|
-
return json as ClientEnvelope;
|
|
330
|
+
return stampReplay(json as ClientEnvelope, res);
|
|
274
331
|
}
|
|
275
332
|
|
|
276
333
|
if (res.ok) {
|
|
277
|
-
return { data: json, error: null };
|
|
334
|
+
return stampReplay({ data: json, error: null }, res);
|
|
278
335
|
}
|
|
279
336
|
|
|
280
337
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
@@ -287,7 +344,64 @@ async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promis
|
|
|
287
344
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
288
345
|
}
|
|
289
346
|
const data = mode === "blob" ? await res.blob() : await res.arrayBuffer();
|
|
290
|
-
return { data, error: null };
|
|
347
|
+
return stampReplay({ data, error: null }, res);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Default `okid()` alphabet, inlined.
|
|
352
|
+
*
|
|
353
|
+
* Same 64-character Base64URL order as {@link OKID_ALPHABET}. Importing
|
|
354
|
+
* `okid` would pull `lazyRequire` and every alphabet constant into the client.
|
|
355
|
+
*/
|
|
356
|
+
export const IDEMPOTENCY_KEY_ALPHABET =
|
|
357
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 21-char key from `crypto.getRandomValues`.
|
|
361
|
+
*
|
|
362
|
+
* `getRandomValues` works on plain HTTP. When `crypto` is missing entirely,
|
|
363
|
+
* returns `undefined` so the call sends no key and runs once.
|
|
364
|
+
*/
|
|
365
|
+
export function mintIdempotencyKey(): string | undefined {
|
|
366
|
+
const webCrypto = globalThis.crypto;
|
|
367
|
+
if (webCrypto?.getRandomValues === undefined) return undefined;
|
|
368
|
+
const bytes = new Uint8Array(21);
|
|
369
|
+
webCrypto.getRandomValues(bytes);
|
|
370
|
+
let out = "";
|
|
371
|
+
for (let i = 0; i < 21; i++) out += IDEMPOTENCY_KEY_ALPHABET[bytes[i]! & 63]!;
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Key for this logical call, or `undefined` when none is sent.
|
|
377
|
+
*
|
|
378
|
+
* @param method - HTTP method, already uppercased
|
|
379
|
+
* @param opt - Per-call override
|
|
380
|
+
*/
|
|
381
|
+
function resolveIdempotencyKey(
|
|
382
|
+
method: string,
|
|
383
|
+
opt: string | false | undefined,
|
|
384
|
+
): string | undefined {
|
|
385
|
+
if (opt === false) return undefined;
|
|
386
|
+
if (typeof opt === "string") return opt;
|
|
387
|
+
if (method === "GET") return undefined;
|
|
388
|
+
return mintIdempotencyKey();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Copy `Idempotent-Replayed: true` onto result meta.
|
|
393
|
+
*
|
|
394
|
+
* @param envelope - Decoded envelope
|
|
395
|
+
* @param res - HTTP response
|
|
396
|
+
*/
|
|
397
|
+
function stampReplay(envelope: ClientEnvelope, res: Response): ClientEnvelope {
|
|
398
|
+
if (res.headers.get("idempotent-replayed") !== "true") return envelope;
|
|
399
|
+
const prev = envelope.meta;
|
|
400
|
+
return { ...envelope, meta: { ...prev, idempotentReplayed: true } };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isAbortError(err: unknown): boolean {
|
|
404
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
291
405
|
}
|
|
292
406
|
|
|
293
407
|
function isTransient(err: unknown): boolean {
|
package/src/client/types.ts
CHANGED
|
@@ -146,6 +146,8 @@ export type ClientListMeta = {
|
|
|
146
146
|
readonly prev?: ClientPagerLink | null;
|
|
147
147
|
readonly total?: number;
|
|
148
148
|
readonly offset?: number;
|
|
149
|
+
/** Set when the server replayed a completed idempotent call. */
|
|
150
|
+
readonly idempotentReplayed?: true;
|
|
149
151
|
};
|
|
150
152
|
|
|
151
153
|
/**
|
|
@@ -163,7 +165,11 @@ export type ClientEnvelope<
|
|
|
163
165
|
readonly error: null;
|
|
164
166
|
readonly meta?: ClientListMeta;
|
|
165
167
|
}
|
|
166
|
-
| {
|
|
168
|
+
| {
|
|
169
|
+
readonly data: null;
|
|
170
|
+
readonly error: ClientError<E> | TransportError;
|
|
171
|
+
readonly meta?: ClientListMeta;
|
|
172
|
+
};
|
|
167
173
|
|
|
168
174
|
/**
|
|
169
175
|
* Client call result — envelope plus always-callable `next` / `prev`.
|
|
@@ -251,7 +257,15 @@ export interface ClientOptions {
|
|
|
251
257
|
readonly headers?: ClientHeaders | (() => ClientHeaders | Promise<ClientHeaders>);
|
|
252
258
|
/** Abort the request after this many milliseconds. */
|
|
253
259
|
readonly timeout?: number;
|
|
254
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* Retry transient failures (network errors, client timeouts, and
|
|
262
|
+
* non-envelope 5xx).
|
|
263
|
+
*
|
|
264
|
+
* A call retries when it is `GET` or `QUERY`, or when it carries an
|
|
265
|
+
* idempotency key. Keys are sent automatically on every non-GET call.
|
|
266
|
+
* `idempotencyKey: false` runs once unless that call passes `{ retry: true }`.
|
|
267
|
+
* `409 IdempotencyInProgress` and `409 JournalLeaseBusy` wait for `Retry-After` and count as an attempt.
|
|
268
|
+
*/
|
|
255
269
|
readonly retry?: {
|
|
256
270
|
/** Extra attempts after the first (default 0). */
|
|
257
271
|
readonly retries?: number;
|
|
@@ -296,11 +310,22 @@ export interface ClientOptions {
|
|
|
296
310
|
export type ClientThenableIterable<T> = PromiseLike<T> & AsyncIterable<T>;
|
|
297
311
|
|
|
298
312
|
/**
|
|
299
|
-
* Per-call options on a Flow invoke (binary decode
|
|
313
|
+
* Per-call options on a Flow invoke (binary decode, abort, retry opt-in).
|
|
300
314
|
*/
|
|
301
315
|
export interface ClientCallOpts {
|
|
302
316
|
readonly response?: "json" | "blob" | "arrayBuffer";
|
|
303
317
|
readonly signal?: AbortSignal;
|
|
318
|
+
/**
|
|
319
|
+
* Repeat this call under the client `retry` policy even when it sends
|
|
320
|
+
* no idempotency key.
|
|
321
|
+
*/
|
|
322
|
+
readonly retry?: boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Stable `Idempotency-Key` for this logical call. A string is sent as-is
|
|
325
|
+
* (one key per form render dedupes double submits). `false` sends no key.
|
|
326
|
+
* Omitted on a non-GET call generates one key and reuses it across retries.
|
|
327
|
+
*/
|
|
328
|
+
readonly idempotencyKey?: string | false;
|
|
304
329
|
}
|
|
305
330
|
|
|
306
331
|
/**
|
|
@@ -503,6 +528,7 @@ export interface ClientDescriptor {
|
|
|
503
528
|
readonly stream?: true;
|
|
504
529
|
readonly matchKey?: readonly string[];
|
|
505
530
|
readonly gates?: readonly string[];
|
|
531
|
+
readonly idempotency?: { readonly mode: "off" | "auto" | "required"; readonly ttl: string };
|
|
506
532
|
};
|
|
507
533
|
};
|
|
508
534
|
};
|
|
@@ -11,9 +11,15 @@ export {
|
|
|
11
11
|
type ResourceStreamOptions,
|
|
12
12
|
type ResourceStreamStop,
|
|
13
13
|
} from "./live-resource.ts";
|
|
14
|
-
export
|
|
14
|
+
export {
|
|
15
|
+
useAgentRun,
|
|
16
|
+
type AgentPendingApproval,
|
|
17
|
+
type AgentRunState,
|
|
18
|
+
type UseAgentRunOptions,
|
|
19
|
+
} from "./use-agent-run.ts";
|
|
15
20
|
export {
|
|
16
21
|
useLiveQuery,
|
|
22
|
+
type LiveRouteContract,
|
|
17
23
|
type UseLiveQueryOptions,
|
|
18
24
|
type UseLiveQueryState,
|
|
19
25
|
} from "./use-live-query.ts";
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAgentRun keeps one follow and does not replay text.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { Window } from "happy-dom";
|
|
7
|
+
import { act, createElement, useEffect } from "react";
|
|
8
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import type { AiMessage } from "../drivers/ai-types.ts";
|
|
10
|
+
import { ai, createAiRuntime } from "../elements/ai.ts";
|
|
11
|
+
import { createGateRuntime, gate } from "../elements/gate.ts";
|
|
12
|
+
import { oke } from "../kernel/app.ts";
|
|
13
|
+
import { flow, resetFlowSeq, type AnyFlowDef } from "../kernel/flow.ts";
|
|
14
|
+
import { createMemoryJournalStore } from "../kernel/journal.ts";
|
|
15
|
+
import { resetBindings, type Binding } from "../kernel/on.ts";
|
|
16
|
+
import { http } from "../kernel/triggers.ts";
|
|
17
|
+
import { useAgentRun, type AgentRunState } from "./use-agent-run.ts";
|
|
18
|
+
|
|
19
|
+
function frames(rows: readonly { id: string; event: unknown }[]): string {
|
|
20
|
+
return rows.map((row) => `id: ${row.id}\ndata: ${JSON.stringify(row.event)}\n\n`).join("");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sse(body: string): Response {
|
|
24
|
+
return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("useAgentRun", () => {
|
|
28
|
+
let happy: Window;
|
|
29
|
+
let root: Root | undefined;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
33
|
+
happy = new Window({ url: "http://app.test/" });
|
|
34
|
+
Object.defineProperty(globalThis, "window", {
|
|
35
|
+
value: happy,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(globalThis, "document", {
|
|
40
|
+
value: happy.document,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
root?.unmount();
|
|
48
|
+
happy.close();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("text is not duplicated and approve does not start a second follow", async () => {
|
|
52
|
+
const encoder = new TextEncoder();
|
|
53
|
+
let follow: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
54
|
+
let lastEventId: string | null = null;
|
|
55
|
+
let follows = 0;
|
|
56
|
+
const fetchFn = Object.assign(
|
|
57
|
+
async (input: string | URL | Request, init?: RequestInit) => {
|
|
58
|
+
const url = String(input);
|
|
59
|
+
if (url.endsWith("/send")) {
|
|
60
|
+
return sse(
|
|
61
|
+
frames([
|
|
62
|
+
{ id: "1", event: { type: "RUN_STARTED", threadId: "t", runId: "run-1" } },
|
|
63
|
+
{ id: "2", event: { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "Hel" } },
|
|
64
|
+
{
|
|
65
|
+
id: "3",
|
|
66
|
+
event: {
|
|
67
|
+
type: "RUN_FINISHED",
|
|
68
|
+
threadId: "t",
|
|
69
|
+
runId: "run-1",
|
|
70
|
+
outcome: {
|
|
71
|
+
type: "interrupt",
|
|
72
|
+
interrupts: [{ id: "ap-1", reason: "approval", payload: { tool: "pay" } }],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (url.endsWith("/events")) {
|
|
80
|
+
follows += 1;
|
|
81
|
+
lastEventId = new Headers(init?.headers).get("last-event-id");
|
|
82
|
+
return new Response(
|
|
83
|
+
new ReadableStream({
|
|
84
|
+
start(controller) {
|
|
85
|
+
follow = controller;
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
follow?.enqueue(
|
|
92
|
+
encoder.encode(
|
|
93
|
+
frames([
|
|
94
|
+
{ id: "4", event: { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "lo" } },
|
|
95
|
+
{
|
|
96
|
+
id: "5",
|
|
97
|
+
event: {
|
|
98
|
+
type: "RUN_FINISHED",
|
|
99
|
+
threadId: "t",
|
|
100
|
+
runId: "run-1",
|
|
101
|
+
result: { cost: 0, stopReason: "completed", output: "Hello" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
follow?.close();
|
|
108
|
+
return new Response(JSON.stringify({ data: { ok: true }, error: null }), { status: 200 });
|
|
109
|
+
},
|
|
110
|
+
{ preconnect: () => undefined },
|
|
111
|
+
) as typeof fetch;
|
|
112
|
+
|
|
113
|
+
let view: AgentRunState | undefined;
|
|
114
|
+
function Probe(): null {
|
|
115
|
+
const state = useAgentRun({
|
|
116
|
+
sendUrl: "http://app.test/send",
|
|
117
|
+
approveUrl: "http://app.test/approve",
|
|
118
|
+
denyUrl: "http://app.test/deny",
|
|
119
|
+
followUrl: (runId) => `http://app.test/runs/${runId}/events`,
|
|
120
|
+
fetch: fetchFn,
|
|
121
|
+
});
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
view = state;
|
|
124
|
+
});
|
|
125
|
+
view = state;
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
root = createRoot(happy.document.createElement("div"));
|
|
130
|
+
await act(async () => {
|
|
131
|
+
root?.render(createElement(Probe));
|
|
132
|
+
});
|
|
133
|
+
await act(async () => {
|
|
134
|
+
await view?.send("hi");
|
|
135
|
+
});
|
|
136
|
+
expect(view?.status).toBe("approval");
|
|
137
|
+
expect(view?.text).toBe("Hel");
|
|
138
|
+
await act(async () => {
|
|
139
|
+
await view?.approve();
|
|
140
|
+
});
|
|
141
|
+
const start = Date.now();
|
|
142
|
+
while (view?.text !== "Hello") {
|
|
143
|
+
if (Date.now() - start > 1_000) throw new Error(`text stayed ${view?.text}`);
|
|
144
|
+
await act(async () => {
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
expect(view?.text).toBe("Hello");
|
|
149
|
+
expect(follows).toBe(1);
|
|
150
|
+
if (lastEventId !== "3") throw new Error(`last event id ${String(lastEventId)}`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a booted app shows the model text once across a real approval", async () => {
|
|
154
|
+
const calls: unknown[] = [];
|
|
155
|
+
const store = createMemoryJournalStore();
|
|
156
|
+
const ops = gate.policy("ops", () => true);
|
|
157
|
+
const runtime = createAiRuntime({
|
|
158
|
+
journalStore: store,
|
|
159
|
+
models: [ai.model("smart")],
|
|
160
|
+
gates: createGateRuntime({ gates: [ops] }),
|
|
161
|
+
agents: [
|
|
162
|
+
ai.agent("support", {
|
|
163
|
+
model: "smart",
|
|
164
|
+
maxSteps: 4,
|
|
165
|
+
tools: [{ name: "refund", approval: true, gate: "ops", timeout: "1h" }],
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
clients: {
|
|
169
|
+
smart: {
|
|
170
|
+
driverId: "mock",
|
|
171
|
+
model: "smart",
|
|
172
|
+
async complete() {
|
|
173
|
+
return { text: "", raw: {}, model: "smart", driverId: "mock" as const };
|
|
174
|
+
},
|
|
175
|
+
async *stream(opts: { messages: readonly AiMessage[] }) {
|
|
176
|
+
const answered = opts.messages.some((message) => message.role === "tool");
|
|
177
|
+
if (!answered) {
|
|
178
|
+
yield { text: "Hello" };
|
|
179
|
+
yield {
|
|
180
|
+
text: "",
|
|
181
|
+
toolCall: { index: 0, id: "tc1", name: "refund", argumentsDelta: "{}" },
|
|
182
|
+
};
|
|
183
|
+
yield { text: "", done: true as const };
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
yield { text: "!" };
|
|
187
|
+
yield { text: "", done: true as const };
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const refund = flow("refund", {
|
|
193
|
+
do: async (input) => {
|
|
194
|
+
calls.push(input);
|
|
195
|
+
return { refunded: true };
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const assist: Binding = {
|
|
199
|
+
trigger: http.post("/assist"),
|
|
200
|
+
flow: flow("assist", {
|
|
201
|
+
durable: true,
|
|
202
|
+
effects: { asks: ["support"], calls: ["refund"] },
|
|
203
|
+
do: (_input, fx) =>
|
|
204
|
+
fx.json.stream(fx.run("support", { message: "refund" }, { stream: true })),
|
|
205
|
+
}) as AnyFlowDef,
|
|
206
|
+
};
|
|
207
|
+
resetBindings();
|
|
208
|
+
const app = oke({
|
|
209
|
+
name: "use-agent-run",
|
|
210
|
+
env: "test",
|
|
211
|
+
startScheduler: false,
|
|
212
|
+
registry: "ignore",
|
|
213
|
+
gate: { unguardedHttp: "allow", policies: [ops] },
|
|
214
|
+
bindings: [assist, { trigger: http.post("/refund"), flow: refund as AnyFlowDef }],
|
|
215
|
+
elements: {
|
|
216
|
+
journal: { store, instanceId: "use-agent-run", leaseMs: 30_000, driverId: "memory" },
|
|
217
|
+
ai: runtime,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
await app.boot({ env: "test" });
|
|
221
|
+
const fetchFn = Object.assign(
|
|
222
|
+
async (input: string | URL | Request, init?: RequestInit) => {
|
|
223
|
+
const request = input instanceof Request ? input : new Request(String(input), init);
|
|
224
|
+
return app.fetch(request);
|
|
225
|
+
},
|
|
226
|
+
{ preconnect: () => undefined },
|
|
227
|
+
) as typeof fetch;
|
|
228
|
+
|
|
229
|
+
let view: AgentRunState | undefined;
|
|
230
|
+
function Probe(): null {
|
|
231
|
+
const state = useAgentRun({
|
|
232
|
+
sendUrl: "http://localhost/assist",
|
|
233
|
+
approveUrl: "http://localhost/agent/approvals/approve",
|
|
234
|
+
denyUrl: "http://localhost/agent/approvals/deny",
|
|
235
|
+
followUrl: (runId) => `http://localhost/agent/runs/${runId}/events`,
|
|
236
|
+
fetch: fetchFn,
|
|
237
|
+
});
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
view = state;
|
|
240
|
+
});
|
|
241
|
+
view = state;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
root = createRoot(happy.document.createElement("div"));
|
|
246
|
+
await act(async () => {
|
|
247
|
+
root?.render(createElement(Probe));
|
|
248
|
+
});
|
|
249
|
+
await act(async () => {
|
|
250
|
+
await view?.send("refund");
|
|
251
|
+
});
|
|
252
|
+
expect(view?.status).toBe("approval");
|
|
253
|
+
expect(view?.text).toBe("Hello");
|
|
254
|
+
await act(async () => {
|
|
255
|
+
await view?.approve();
|
|
256
|
+
});
|
|
257
|
+
await app.resumeDurable(Date.now() + 1000);
|
|
258
|
+
const start = Date.now();
|
|
259
|
+
while (view?.text !== "Hello!") {
|
|
260
|
+
if (Date.now() - start > 3_000) throw new Error(`text stayed ${view?.text}`);
|
|
261
|
+
await act(async () => {
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
expect(view?.text).toBe("Hello!");
|
|
266
|
+
expect(calls).toEqual([{}]);
|
|
267
|
+
await app.bootResult?.close();
|
|
268
|
+
resetBindings();
|
|
269
|
+
resetFlowSeq();
|
|
270
|
+
});
|
|
271
|
+
});
|