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
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
import type { AiDriver, AiMessage, AiModelClient, AiToolDef } from "../../drivers/ai-types.ts";
|
|
11
11
|
import { currentAbortSignal, withAbortSignal } from "../../kernel/abort-scope.ts";
|
|
12
|
+
import { hasJournalLease, type JournalSession, type JournalStore } from "../../kernel/journal.ts";
|
|
13
|
+
import { isJournalSuspend } from "../../kernel/journal-suspend.ts";
|
|
14
|
+
import {
|
|
15
|
+
AiDurableRequiredError,
|
|
16
|
+
approvalId,
|
|
17
|
+
approvalStepName,
|
|
18
|
+
approvalTimeoutMs,
|
|
19
|
+
readAgentApproval,
|
|
20
|
+
resolveAgentApproval,
|
|
21
|
+
type AgentApprovalDecision,
|
|
22
|
+
type AgentApprovalRecord,
|
|
23
|
+
type AgentApprovalResolveResult,
|
|
24
|
+
} from "./approval.ts";
|
|
12
25
|
import {
|
|
13
26
|
mcpCapabilityRefFromName,
|
|
14
27
|
mcpModelToolName,
|
|
@@ -18,6 +31,7 @@ import { createMcpClient, type McpClient } from "./mcp-client.ts";
|
|
|
18
31
|
import type { McpTransport } from "./mcp-transport.ts";
|
|
19
32
|
import type { IndexStore } from "../../drivers/types.ts";
|
|
20
33
|
import { maskRedactedDeep } from "../../kernel/redacted.ts";
|
|
34
|
+
import { lazyRequire } from "../../kernel/lazy-require.ts";
|
|
21
35
|
import type { GatePolicyContext } from "../gate/declare.ts";
|
|
22
36
|
import type { GateRuntime } from "../gate/runtime.ts";
|
|
23
37
|
import type {
|
|
@@ -28,12 +42,20 @@ import type {
|
|
|
28
42
|
AiPromptDecl,
|
|
29
43
|
AiTimeout,
|
|
30
44
|
} from "./declare.ts";
|
|
45
|
+
import type { AgentEventLog, AgentRunHeader } from "./run-events.ts";
|
|
46
|
+
import { setAgentEventLog } from "./agent-event-slot.ts";
|
|
31
47
|
import {
|
|
32
48
|
isRetryableAiError,
|
|
33
49
|
mergeAskAbortSignal,
|
|
34
50
|
outExpectsVia,
|
|
35
51
|
resolveTimeoutMs,
|
|
36
52
|
} from "./errors.ts";
|
|
53
|
+
import {
|
|
54
|
+
createEventQueue,
|
|
55
|
+
emitAssistantText,
|
|
56
|
+
type AgentEventEmit,
|
|
57
|
+
type AgUiEvent,
|
|
58
|
+
} from "./events.ts";
|
|
37
59
|
import {
|
|
38
60
|
AiSchemaValidationError,
|
|
39
61
|
coerceModelObject,
|
|
@@ -49,6 +71,78 @@ const AI_SAME_MODEL_RETRY_BACKOFF_MS = 250;
|
|
|
49
71
|
/** Default bound for tool / agent loops. */
|
|
50
72
|
export const AI_DEFAULT_MAX_STEPS = 6;
|
|
51
73
|
|
|
74
|
+
/** Model-turn reader. Stream parsing stays off the AI barrel until a turn runs. */
|
|
75
|
+
function loadStreamTurn(): typeof import("./stream-turn.ts") {
|
|
76
|
+
return lazyRequire(import.meta.dir, ["stream", "turn"].join("-"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Follow log. A static import pulls the journal event store onto every AI import. */
|
|
80
|
+
function loadRunEvents(): typeof import("./run-events.ts") {
|
|
81
|
+
return lazyRequire(import.meta.dir, ["run", "events"].join("-"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Approval HTTP routes. Bound when an AI runtime is created, not at import. */
|
|
85
|
+
function loadApprovalHttp(): typeof import("./approval-http.ts") {
|
|
86
|
+
return lazyRequire(import.meta.dir, ["approval", "http"].join("-"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Run ids. The extended okid table stays off this graph. */
|
|
90
|
+
function loadOkid(): typeof import("../../okid.ts") {
|
|
91
|
+
return lazyRequire(`${import.meta.dir}/../..`, ["ok", "id"].join(""));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** SSE id stamp for a followed event. */
|
|
95
|
+
function loadSseId(): typeof import("../../kernel/sse-id.ts") {
|
|
96
|
+
return lazyRequire(`${import.meta.dir}/../../kernel`, ["sse", "id"].join("-"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Thrown inside the tool loop when a deny or abort ends the run. */
|
|
100
|
+
class AgentLoopHalt extends Error {
|
|
101
|
+
readonly stopReason: "aborted" | "denied" | "error";
|
|
102
|
+
override readonly cause: unknown;
|
|
103
|
+
readonly trail: readonly AgentToolStep[];
|
|
104
|
+
readonly denials: readonly AgentDenial[];
|
|
105
|
+
readonly steps: number;
|
|
106
|
+
readonly cost: number;
|
|
107
|
+
readonly output: unknown;
|
|
108
|
+
|
|
109
|
+
constructor(
|
|
110
|
+
stopReason: "aborted" | "denied" | "error",
|
|
111
|
+
cause: unknown,
|
|
112
|
+
partial: {
|
|
113
|
+
readonly trail: readonly AgentToolStep[];
|
|
114
|
+
readonly denials: readonly AgentDenial[];
|
|
115
|
+
readonly steps: number;
|
|
116
|
+
readonly cost: number;
|
|
117
|
+
readonly output: unknown;
|
|
118
|
+
},
|
|
119
|
+
) {
|
|
120
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
121
|
+
this.name = stopReason === "aborted" ? "AbortError" : "AgentLoopHalt";
|
|
122
|
+
this.stopReason = stopReason;
|
|
123
|
+
this.cause = cause;
|
|
124
|
+
this.trail = partial.trail;
|
|
125
|
+
this.denials = partial.denials;
|
|
126
|
+
this.steps = partial.steps;
|
|
127
|
+
this.cost = partial.cost;
|
|
128
|
+
this.output = partial.output;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Console observability cap for ask journal entries and agent run records. */
|
|
133
|
+
export const AI_OBSERVABILITY_LIMIT = 500;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Keep the newest entries. Older rows are observability, not storage.
|
|
137
|
+
*
|
|
138
|
+
* @param buf - Mutable ring
|
|
139
|
+
* @param item - Entry to append
|
|
140
|
+
*/
|
|
141
|
+
function pushObservability<T>(buf: T[], item: T): void {
|
|
142
|
+
buf.push(item);
|
|
143
|
+
if (buf.length > AI_OBSERVABILITY_LIMIT) buf.shift();
|
|
144
|
+
}
|
|
145
|
+
|
|
52
146
|
/**
|
|
53
147
|
* Split `name` / `name@version` the same way capability pins do.
|
|
54
148
|
*
|
|
@@ -65,6 +159,9 @@ export function parsePromptRef(ref: string): {
|
|
|
65
159
|
return { name: ref.slice(0, at), version: Number(tail) };
|
|
66
160
|
}
|
|
67
161
|
|
|
162
|
+
/** Why an agent loop stopped. A fed-back denial is not `denied`. */
|
|
163
|
+
export type AgentStopReason = "completed" | "max_steps" | "budget" | "denied" | "aborted" | "error";
|
|
164
|
+
|
|
68
165
|
/** Recorded agent tool denial (containment proof — not an error). */
|
|
69
166
|
export interface AgentDenial {
|
|
70
167
|
readonly agent: string;
|
|
@@ -90,6 +187,8 @@ export interface AgentToolStep {
|
|
|
90
187
|
readonly effects: readonly AgentToolEffect[];
|
|
91
188
|
readonly denial?: AgentDenial;
|
|
92
189
|
readonly at: number;
|
|
190
|
+
/** Who approved this tool, when a human resolution ran it. */
|
|
191
|
+
readonly approver?: string;
|
|
93
192
|
}
|
|
94
193
|
|
|
95
194
|
/** Full agent run recorded on the denial / trail ledger. */
|
|
@@ -98,12 +197,25 @@ export interface AgentRunRecord {
|
|
|
98
197
|
readonly agent: string;
|
|
99
198
|
readonly message: string;
|
|
100
199
|
readonly ok: boolean;
|
|
200
|
+
readonly stopReason: AgentStopReason;
|
|
101
201
|
readonly steps: number;
|
|
102
202
|
readonly trail: readonly AgentToolStep[];
|
|
103
203
|
readonly denials: readonly AgentDenial[];
|
|
104
204
|
readonly output?: unknown;
|
|
105
205
|
readonly at: number;
|
|
106
206
|
readonly cost: number;
|
|
207
|
+
/** Parent agent run, when this run is a nested tool. */
|
|
208
|
+
readonly parentRunId?: string;
|
|
209
|
+
/** Message when {@link stopReason} is `error`. */
|
|
210
|
+
readonly error?: string;
|
|
211
|
+
/** AG-UI thread, when the run opened a follow log. */
|
|
212
|
+
readonly threadId?: string;
|
|
213
|
+
/** Epoch ms the run finished. Absent while it is still open. */
|
|
214
|
+
readonly finishedAt?: number;
|
|
215
|
+
/** Driver-reported input tokens. Omitted when the driver did not supply them. */
|
|
216
|
+
readonly inputTokens?: number;
|
|
217
|
+
/** Driver-reported output tokens. Omitted when the driver did not supply them. */
|
|
218
|
+
readonly outputTokens?: number;
|
|
107
219
|
}
|
|
108
220
|
|
|
109
221
|
/** Fallback attempt for model routing (`via` chains). */
|
|
@@ -201,6 +313,10 @@ export interface CreateAiRuntimeOptions {
|
|
|
201
313
|
* @param input - Tool input
|
|
202
314
|
*/
|
|
203
315
|
readonly callFlow?: (name: string, input: unknown) => Promise<unknown>;
|
|
316
|
+
/** Journal store that holds pending tool approvals. */
|
|
317
|
+
readonly journalStore?: JournalStore;
|
|
318
|
+
/** Event log for follow / resume. Defaults to an in-memory log. */
|
|
319
|
+
readonly eventLog?: AgentEventLog;
|
|
204
320
|
/**
|
|
205
321
|
* Resolve gates required for a tool flow.
|
|
206
322
|
*
|
|
@@ -251,16 +367,46 @@ export interface AiAskOptions {
|
|
|
251
367
|
* Falls back to runtime `callFlow` when omitted.
|
|
252
368
|
*/
|
|
253
369
|
readonly callTool?: (name: string, input: unknown) => Promise<unknown>;
|
|
370
|
+
/** Yield AG-UI events instead of returning the validated object. */
|
|
371
|
+
readonly stream?: boolean;
|
|
254
372
|
}
|
|
255
373
|
|
|
256
374
|
/** Agent run options. */
|
|
257
375
|
export interface AiAgentRunOptions {
|
|
258
|
-
|
|
376
|
+
/** Single user turn. Mutually exclusive with {@link messages}. */
|
|
377
|
+
readonly message?: string;
|
|
378
|
+
/** Prior user, assistant, and tool turns. Mutually exclusive with {@link message}. */
|
|
379
|
+
readonly messages?: readonly AiMessage[];
|
|
259
380
|
readonly auth?: GatePolicyContext["auth"];
|
|
260
381
|
readonly operator?: GatePolicyContext["operator"];
|
|
261
382
|
readonly meta?: GatePolicyContext["meta"];
|
|
262
383
|
/** Host-flow dispatch — must be `fx.call` when wired from fx.run. */
|
|
263
384
|
readonly callTool?: (name: string, input: unknown) => Promise<unknown>;
|
|
385
|
+
/** Durable journal when the calling Flow is `durable: true`. */
|
|
386
|
+
readonly journal?: JournalSession;
|
|
387
|
+
/** Calling flow name, used when approval requires durability. */
|
|
388
|
+
readonly flow?: string;
|
|
389
|
+
readonly tenantId?: string | null;
|
|
390
|
+
/** This run's id. Nested tools stamp it as `parentRunId`. */
|
|
391
|
+
readonly runId?: string;
|
|
392
|
+
/** Parent agent run id. */
|
|
393
|
+
readonly parentRunId?: string;
|
|
394
|
+
/** 1-based nest level. Default 1. */
|
|
395
|
+
readonly depth?: number;
|
|
396
|
+
/** Cost cap for this invocation. Wins over the agent's own budget. */
|
|
397
|
+
readonly maxCostPerRun?: number;
|
|
398
|
+
/** Record a `call` effect on the host ledger when a child agent starts. */
|
|
399
|
+
readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
|
|
400
|
+
/** Fires once the agent run id exists, including streamed runs. */
|
|
401
|
+
readonly onRunId?: (runId: string) => void;
|
|
402
|
+
/** AG-UI thread. Defaults to a unique id. */
|
|
403
|
+
readonly threadId?: string;
|
|
404
|
+
/** Non-public gates on the calling Flow. */
|
|
405
|
+
readonly gates?: readonly string[];
|
|
406
|
+
/** Starting `auth.userId`. */
|
|
407
|
+
readonly userId?: string | null;
|
|
408
|
+
/** Starting operator id. */
|
|
409
|
+
readonly operatorId?: string | null;
|
|
264
410
|
}
|
|
265
411
|
|
|
266
412
|
/** Stream options. */
|
|
@@ -285,7 +431,7 @@ export interface AiRuntime {
|
|
|
285
431
|
readonly denials: readonly AgentDenial[];
|
|
286
432
|
/** Agent runs with full tool trails. */
|
|
287
433
|
readonly agentRuns: readonly AgentRunRecord[];
|
|
288
|
-
/**
|
|
434
|
+
/** Ask results kept for Console (last {@link AI_OBSERVABILITY_LIMIT}). Not a replay cache. */
|
|
289
435
|
readonly journal: readonly AiJournalEntry[];
|
|
290
436
|
/**
|
|
291
437
|
* Egress identity from the most recent ask / embed / stream complete.
|
|
@@ -300,6 +446,14 @@ export interface AiRuntime {
|
|
|
300
446
|
* @param opts - via / tools / callTool / allowPii
|
|
301
447
|
*/
|
|
302
448
|
ask(prompt: string, input?: unknown, opts?: AiAskOptions): Promise<Record<string, unknown>>;
|
|
449
|
+
/**
|
|
450
|
+
* Ask and yield AG-UI events. Tool-less asks still throw on budget inside the stream.
|
|
451
|
+
*
|
|
452
|
+
* @param prompt - Prompt name
|
|
453
|
+
* @param input - Prompt input
|
|
454
|
+
* @param opts - via / tools / callTool
|
|
455
|
+
*/
|
|
456
|
+
streamAsk(prompt: string, input?: unknown, opts?: AiAskOptions): AsyncIterable<AgUiEvent>;
|
|
303
457
|
/**
|
|
304
458
|
* Run a bounded agent; tool calls that fail gates are denied + recorded.
|
|
305
459
|
*
|
|
@@ -311,12 +465,22 @@ export interface AiRuntime {
|
|
|
311
465
|
options: AiAgentRunOptions,
|
|
312
466
|
): Promise<{
|
|
313
467
|
readonly ok: boolean;
|
|
468
|
+
readonly stopReason: AgentStopReason;
|
|
314
469
|
readonly steps: number;
|
|
315
470
|
readonly denials: readonly AgentDenial[];
|
|
316
471
|
readonly trail: readonly AgentToolStep[];
|
|
317
472
|
readonly output?: unknown;
|
|
318
473
|
readonly cost: number;
|
|
474
|
+
readonly inputTokens?: number;
|
|
475
|
+
readonly outputTokens?: number;
|
|
319
476
|
}>;
|
|
477
|
+
/**
|
|
478
|
+
* Run an agent and yield AG-UI events.
|
|
479
|
+
*
|
|
480
|
+
* @param agent - Agent name
|
|
481
|
+
* @param options - Message + auth context
|
|
482
|
+
*/
|
|
483
|
+
streamAgent(agent: string, options: AiAgentRunOptions): AsyncIterable<AgUiEvent>;
|
|
320
484
|
/**
|
|
321
485
|
* Stream model tokens (real driver stream; fails loud if unsupported).
|
|
322
486
|
*
|
|
@@ -348,6 +512,18 @@ export interface AiRuntime {
|
|
|
348
512
|
* @param text - Text to embed
|
|
349
513
|
*/
|
|
350
514
|
embedVector(model: string, text: string): Promise<readonly number[]>;
|
|
515
|
+
/**
|
|
516
|
+
* Approve or deny a pending tool. First resolution wins.
|
|
517
|
+
*
|
|
518
|
+
* @param id - Approval id
|
|
519
|
+
* @param decision - Approve or deny
|
|
520
|
+
* @param ctx - Gate context for the tool's gate
|
|
521
|
+
*/
|
|
522
|
+
resolveApproval(
|
|
523
|
+
id: string,
|
|
524
|
+
decision: AgentApprovalDecision,
|
|
525
|
+
ctx: GatePolicyContext,
|
|
526
|
+
): Promise<AgentApprovalResolveResult>;
|
|
351
527
|
}
|
|
352
528
|
|
|
353
529
|
/**
|
|
@@ -362,11 +538,79 @@ export function promptContentFromInput(input: unknown): string {
|
|
|
362
538
|
}
|
|
363
539
|
|
|
364
540
|
/**
|
|
365
|
-
*
|
|
541
|
+
* Initial model messages for an agent run.
|
|
542
|
+
*
|
|
543
|
+
* @param runOpts - Single message or a history
|
|
544
|
+
*/
|
|
545
|
+
function agentMessages(runOpts: AiAgentRunOptions): AiMessage[] {
|
|
546
|
+
if (runOpts.message !== undefined && runOpts.messages !== undefined) {
|
|
547
|
+
throw new TypeError("fx.run: pass message or messages, not both");
|
|
548
|
+
}
|
|
549
|
+
if (runOpts.messages !== undefined) return [...runOpts.messages];
|
|
550
|
+
return [{ role: "user", content: promptContentFromInput(runOpts.message ?? "") }];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const agentRunSlots = new WeakMap<JournalSession, { epoch: number; slot: number }>();
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Run id for one agent invocation. A durable Flow journals the id so two
|
|
557
|
+
* streamed runs in the same Flow do not share a log, and a replay keeps it.
|
|
558
|
+
*
|
|
559
|
+
* @param agent - Agent name
|
|
560
|
+
* @param runOpts - Run options
|
|
561
|
+
*/
|
|
562
|
+
async function allocateAgentRunId(agent: string, runOpts: AiAgentRunOptions): Promise<string> {
|
|
563
|
+
if (runOpts.runId) return runOpts.runId;
|
|
564
|
+
const journal = runOpts.journal;
|
|
565
|
+
if (!journal) return loadOkid().okid();
|
|
566
|
+
const epoch = journal.epoch;
|
|
567
|
+
const prev = agentRunSlots.get(journal);
|
|
568
|
+
const next = prev && prev.epoch === epoch ? prev.slot + 1 : 1;
|
|
569
|
+
agentRunSlots.set(journal, { epoch, slot: next });
|
|
570
|
+
const stored = await journal.effect("ask", `oke.agent.run.${agent}.${next}`, () =>
|
|
571
|
+
loadOkid().okid(),
|
|
572
|
+
);
|
|
573
|
+
return typeof stored === "string" ? stored : loadOkid().okid();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Principal and gates stored on the follow log.
|
|
578
|
+
*
|
|
579
|
+
* @param runId - Agent run id
|
|
580
|
+
* @param threadId - AG-UI thread
|
|
581
|
+
* @param runOpts - Run options
|
|
582
|
+
*/
|
|
583
|
+
function agentLogHeader(
|
|
584
|
+
runId: string,
|
|
585
|
+
threadId: string,
|
|
586
|
+
agent: string,
|
|
587
|
+
runOpts: AiAgentRunOptions,
|
|
588
|
+
): AgentRunHeader {
|
|
589
|
+
return {
|
|
590
|
+
runId,
|
|
591
|
+
threadId,
|
|
592
|
+
agent,
|
|
593
|
+
...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
|
|
594
|
+
tenant: runOpts.tenantId ?? null,
|
|
595
|
+
gates: runOpts.gates ?? [],
|
|
596
|
+
userId: runOpts.userId ?? runOpts.auth?.userId ?? null,
|
|
597
|
+
operatorId: runOpts.operatorId ?? runOpts.operator?.id ?? null,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Ledger label for a run that may be a history rather than one string.
|
|
366
603
|
*
|
|
367
|
-
* @param
|
|
368
|
-
* @param out - Prompt output schema
|
|
604
|
+
* @param runOpts - Single message or a history
|
|
369
605
|
*/
|
|
606
|
+
function agentMessageLabel(runOpts: AiAgentRunOptions): string {
|
|
607
|
+
if (typeof runOpts.message === "string") return runOpts.message;
|
|
608
|
+
const lastUser = [...(runOpts.messages ?? [])]
|
|
609
|
+
.reverse()
|
|
610
|
+
.find((message) => message.role === "user");
|
|
611
|
+
return lastUser?.content ?? "";
|
|
612
|
+
}
|
|
613
|
+
|
|
370
614
|
function askUserContent(input: unknown, out: unknown): string {
|
|
371
615
|
const base = promptContentFromInput(input);
|
|
372
616
|
const schema = promptOutJsonSchema(out);
|
|
@@ -390,6 +634,23 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
390
634
|
for (const m of options.models ?? []) models.set(m.name, m);
|
|
391
635
|
|
|
392
636
|
const clients = new Map<string, AiModelClient>(Object.entries(options.clients ?? {}));
|
|
637
|
+
const eventStore = options.journalStore?.agentEvents;
|
|
638
|
+
const eventLog =
|
|
639
|
+
options.eventLog ??
|
|
640
|
+
loadRunEvents().createMemoryAgentEventLog(eventStore, {
|
|
641
|
+
claim: async (runId) => {
|
|
642
|
+
const journal = options.journalStore;
|
|
643
|
+
if (journal && hasJournalLease(journal)) {
|
|
644
|
+
const row = await journal.get(runId);
|
|
645
|
+
if (row) {
|
|
646
|
+
return journal.acquireLease(runId, "agent-events", Date.now(), 30_000);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return (await eventStore?.claim(runId)) ?? true;
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
setAgentEventLog(eventLog);
|
|
653
|
+
loadApprovalHttp().setAgentFollowGates(options.gates);
|
|
393
654
|
const mcpClient: McpClient = createMcpClient({
|
|
394
655
|
servers: options.mcpServers,
|
|
395
656
|
...(options.resolveSecret !== undefined ? { resolveSecret: options.resolveSecret } : {}),
|
|
@@ -399,6 +660,27 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
399
660
|
const agentRuns: AgentRunRecord[] = [];
|
|
400
661
|
const journal: AiJournalEntry[] = [];
|
|
401
662
|
const now = options.now ?? (() => Date.now());
|
|
663
|
+
const noteAppendFailure = (
|
|
664
|
+
runId: string,
|
|
665
|
+
agentName: string,
|
|
666
|
+
label: string,
|
|
667
|
+
err: unknown,
|
|
668
|
+
): void => {
|
|
669
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
670
|
+
pushObservability(agentRuns, {
|
|
671
|
+
id: runId,
|
|
672
|
+
agent: agentName,
|
|
673
|
+
message: label,
|
|
674
|
+
ok: false,
|
|
675
|
+
stopReason: "error",
|
|
676
|
+
error: message,
|
|
677
|
+
steps: 0,
|
|
678
|
+
trail: [],
|
|
679
|
+
denials: [],
|
|
680
|
+
at: now(),
|
|
681
|
+
cost: 0,
|
|
682
|
+
});
|
|
683
|
+
};
|
|
402
684
|
const journalingForced = options.forceJournal !== false;
|
|
403
685
|
let runSeq = 0;
|
|
404
686
|
/** Mutable egress stamp shared by ask / embed / toolLoop. */
|
|
@@ -485,6 +767,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
485
767
|
return defs;
|
|
486
768
|
}
|
|
487
769
|
|
|
770
|
+
let self: AiRuntime;
|
|
771
|
+
|
|
488
772
|
async function dispatchTool(opts: {
|
|
489
773
|
readonly tool: string;
|
|
490
774
|
readonly args: unknown;
|
|
@@ -497,6 +781,21 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
497
781
|
readonly trail: AgentToolStep[];
|
|
498
782
|
readonly runDenials: AgentDenial[];
|
|
499
783
|
readonly signal?: AbortSignal;
|
|
784
|
+
readonly callId?: string;
|
|
785
|
+
readonly step?: number;
|
|
786
|
+
readonly index?: number;
|
|
787
|
+
readonly threadId?: string;
|
|
788
|
+
readonly journal?: JournalSession;
|
|
789
|
+
readonly flow?: string;
|
|
790
|
+
readonly tenantId?: string | null;
|
|
791
|
+
readonly approvals?: AiAgentDecl["approvals"];
|
|
792
|
+
readonly emit?: AgentEventEmit;
|
|
793
|
+
readonly runId?: string;
|
|
794
|
+
readonly depth?: number;
|
|
795
|
+
readonly maxCostPerRun?: number;
|
|
796
|
+
readonly spent?: number;
|
|
797
|
+
readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
|
|
798
|
+
readonly spend?: { cost: number; inputTokens?: number; outputTokens?: number };
|
|
500
799
|
}): Promise<unknown> {
|
|
501
800
|
const {
|
|
502
801
|
tool,
|
|
@@ -525,7 +824,9 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
525
824
|
runDenials.push(denial);
|
|
526
825
|
denials.push(denial);
|
|
527
826
|
trail.push({ tool, status: "denied", effects, denial, at: denial.at });
|
|
528
|
-
|
|
827
|
+
const unknown = new Error(`ai: model requested unknown tool "${tool}"`);
|
|
828
|
+
unknown.name = "AgentDenied";
|
|
829
|
+
throw unknown;
|
|
529
830
|
}
|
|
530
831
|
|
|
531
832
|
const requiredGates = options.gatesForFlow?.(capability) ?? [];
|
|
@@ -552,6 +853,153 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
552
853
|
}
|
|
553
854
|
}
|
|
554
855
|
|
|
856
|
+
const approval = opts.approvals?.[capability] ?? opts.approvals?.[tool];
|
|
857
|
+
const needsApproval =
|
|
858
|
+
approval !== undefined &&
|
|
859
|
+
(approval.when === undefined || approval.when(args, { auth, tenant: opts.tenantId ?? null }));
|
|
860
|
+
if (needsApproval) {
|
|
861
|
+
if (!opts.journal) {
|
|
862
|
+
throw new AiDurableRequiredError(opts.flow ?? "(unknown)", agentLabel);
|
|
863
|
+
}
|
|
864
|
+
const toolCallId =
|
|
865
|
+
opts.callId && opts.callId.length > 0
|
|
866
|
+
? opts.callId
|
|
867
|
+
: `${agentLabel}:${opts.step ?? 0}:${opts.index ?? 0}`;
|
|
868
|
+
const id = approvalId(opts.journal.runId, toolCallId);
|
|
869
|
+
const record: AgentApprovalRecord = {
|
|
870
|
+
status: "pending",
|
|
871
|
+
agent: agentLabel,
|
|
872
|
+
tool: capability,
|
|
873
|
+
args,
|
|
874
|
+
gate: approval.gate,
|
|
875
|
+
tenant: opts.tenantId ?? null,
|
|
876
|
+
requestedAt: now(),
|
|
877
|
+
};
|
|
878
|
+
const stored = (await opts.journal.step(
|
|
879
|
+
approvalStepName(id),
|
|
880
|
+
() => record,
|
|
881
|
+
)) as AgentApprovalRecord;
|
|
882
|
+
if (stored.status === "pending") {
|
|
883
|
+
opts.emit?.({
|
|
884
|
+
type: "RUN_FINISHED",
|
|
885
|
+
threadId: opts.threadId ?? opts.runId ?? opts.journal.runId,
|
|
886
|
+
runId: opts.runId ?? opts.journal.runId,
|
|
887
|
+
outcome: {
|
|
888
|
+
type: "interrupt",
|
|
889
|
+
interrupts: [{ id, reason: "approval", payload: { tool: capability, args } }],
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
// Always consume the sleep row. Resolve moves wakeAt to now, so a
|
|
894
|
+
// resolved replay returns here instead of parking again.
|
|
895
|
+
await opts.journal.sleep(approvalStepName(id), approval.timeout, () =>
|
|
896
|
+
approvalTimeoutMs(approval.timeout),
|
|
897
|
+
);
|
|
898
|
+
const store = options.journalStore;
|
|
899
|
+
if (!store) throw new Error("ai: approval resume requires a journal store");
|
|
900
|
+
let decision = (await readAgentApproval(store, id)) ?? stored;
|
|
901
|
+
if (decision.status === "pending") {
|
|
902
|
+
const wrote = await resolveAgentApproval(
|
|
903
|
+
store,
|
|
904
|
+
id,
|
|
905
|
+
{ decision: "deny", reason: "timeout", tenant: decision.tenant },
|
|
906
|
+
now,
|
|
907
|
+
opts.journal.run.lockedBy,
|
|
908
|
+
);
|
|
909
|
+
decision = wrote.ok
|
|
910
|
+
? { ...decision, status: "denied", reason: "timeout" }
|
|
911
|
+
: ((await readAgentApproval(store, id)) ?? decision);
|
|
912
|
+
}
|
|
913
|
+
if (decision.status === "denied") {
|
|
914
|
+
const denial: AgentDenial = {
|
|
915
|
+
agent: agentLabel,
|
|
916
|
+
tool: capability,
|
|
917
|
+
gate: approval.gate,
|
|
918
|
+
reason: decision.reason ?? "denied",
|
|
919
|
+
at: now(),
|
|
920
|
+
};
|
|
921
|
+
runDenials.push(denial);
|
|
922
|
+
denials.push(denial);
|
|
923
|
+
trail.push({ tool: capability, status: "denied", effects, denial, at: denial.at });
|
|
924
|
+
return { denied: true, reason: denial.reason };
|
|
925
|
+
}
|
|
926
|
+
const toolArgs = decision.editedArgs !== undefined ? decision.editedArgs : args;
|
|
927
|
+
const output = await opts.journal.effect("call", approvalStepName(id), () => {
|
|
928
|
+
const call = callTool ?? options.callFlow;
|
|
929
|
+
if (!call) throw new Error("callFlow not configured");
|
|
930
|
+
return withAbortSignal(signal ?? currentAbortSignal(), () => call(capability, toolArgs));
|
|
931
|
+
});
|
|
932
|
+
trail.push({
|
|
933
|
+
tool: capability,
|
|
934
|
+
status: "ok",
|
|
935
|
+
effects,
|
|
936
|
+
at: now(),
|
|
937
|
+
...(decision.approver !== undefined ? { approver: decision.approver } : {}),
|
|
938
|
+
});
|
|
939
|
+
return output;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const childDecl = agents.get(capability) ?? agents.get(tool);
|
|
943
|
+
if (childDecl && self) {
|
|
944
|
+
const depth = opts.depth ?? 1;
|
|
945
|
+
const parentLimit = agents.get(agentLabel)?.maxDepth ?? 3;
|
|
946
|
+
if (depth >= parentLimit) {
|
|
947
|
+
return { error: `ai: agent "${agentLabel}" is nested past maxDepth ${parentLimit}` };
|
|
948
|
+
}
|
|
949
|
+
const childId = loadOkid().okid();
|
|
950
|
+
opts.emit?.({
|
|
951
|
+
type: "CUSTOM",
|
|
952
|
+
name: "oke.subagent.started",
|
|
953
|
+
value: { runId: childId, parentToolCallId: opts.callId },
|
|
954
|
+
});
|
|
955
|
+
await opts.recordCall?.(childDecl.name, childId);
|
|
956
|
+
const parentRemaining =
|
|
957
|
+
opts.maxCostPerRun !== undefined ? opts.maxCostPerRun - (opts.spent ?? 0) : undefined;
|
|
958
|
+
const childCap = childDecl.budget?.maxCostPerRun;
|
|
959
|
+
const cap =
|
|
960
|
+
parentRemaining !== undefined
|
|
961
|
+
? childCap !== undefined
|
|
962
|
+
? Math.min(childCap, parentRemaining)
|
|
963
|
+
: parentRemaining
|
|
964
|
+
: childCap;
|
|
965
|
+
try {
|
|
966
|
+
const child = await self.runAgent(childDecl.name, {
|
|
967
|
+
message: typeof args === "string" ? args : JSON.stringify(args ?? {}),
|
|
968
|
+
runId: childId,
|
|
969
|
+
parentRunId: opts.runId,
|
|
970
|
+
depth: depth + 1,
|
|
971
|
+
...(cap !== undefined ? { maxCostPerRun: cap } : {}),
|
|
972
|
+
...(callTool !== undefined ? { callTool } : {}),
|
|
973
|
+
...(auth !== undefined ? { auth } : {}),
|
|
974
|
+
...(operator !== undefined ? { operator } : {}),
|
|
975
|
+
...(meta !== undefined ? { meta } : {}),
|
|
976
|
+
...(opts.journal !== undefined ? { journal: opts.journal } : {}),
|
|
977
|
+
...(opts.flow !== undefined ? { flow: opts.flow } : {}),
|
|
978
|
+
...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
|
|
979
|
+
...(opts.recordCall !== undefined ? { recordCall: opts.recordCall } : {}),
|
|
980
|
+
});
|
|
981
|
+
if (opts.spend) {
|
|
982
|
+
opts.spend.cost += child.cost;
|
|
983
|
+
opts.spend.inputTokens = (opts.spend.inputTokens ?? 0) + (child.inputTokens ?? 0);
|
|
984
|
+
opts.spend.outputTokens = (opts.spend.outputTokens ?? 0) + (child.outputTokens ?? 0);
|
|
985
|
+
}
|
|
986
|
+
opts.emit?.({
|
|
987
|
+
type: "CUSTOM",
|
|
988
|
+
name: "oke.subagent.finished",
|
|
989
|
+
value: { runId: childId, parentToolCallId: opts.callId },
|
|
990
|
+
});
|
|
991
|
+
trail.push({ tool: capability, status: "ok", effects, at: now() });
|
|
992
|
+
return child.output ?? child;
|
|
993
|
+
} catch (err) {
|
|
994
|
+
opts.emit?.({
|
|
995
|
+
type: "CUSTOM",
|
|
996
|
+
name: "oke.subagent.error",
|
|
997
|
+
value: { runId: childId, parentToolCallId: opts.callId },
|
|
998
|
+
});
|
|
999
|
+
throw err;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
555
1003
|
const invoke = callTool ?? options.callFlow;
|
|
556
1004
|
if (!invoke) {
|
|
557
1005
|
const denial: AgentDenial = {
|
|
@@ -579,6 +1027,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
579
1027
|
readonly messages: AiMessage[];
|
|
580
1028
|
readonly tools: readonly string[];
|
|
581
1029
|
readonly maxSteps: number;
|
|
1030
|
+
/** Stop before the next model call once accumulated cost reaches this cap. */
|
|
1031
|
+
readonly maxCostPerRun?: number;
|
|
582
1032
|
readonly agentLabel: string;
|
|
583
1033
|
readonly responseFormat?: unknown;
|
|
584
1034
|
readonly signal?: AbortSignal;
|
|
@@ -586,6 +1036,15 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
586
1036
|
readonly auth?: GatePolicyContext["auth"];
|
|
587
1037
|
readonly operator?: GatePolicyContext["operator"];
|
|
588
1038
|
readonly meta?: GatePolicyContext["meta"];
|
|
1039
|
+
readonly emit?: AgentEventEmit;
|
|
1040
|
+
readonly journal?: JournalSession;
|
|
1041
|
+
readonly flow?: string;
|
|
1042
|
+
readonly tenantId?: string | null;
|
|
1043
|
+
readonly approvals?: AiAgentDecl["approvals"];
|
|
1044
|
+
readonly runId?: string;
|
|
1045
|
+
readonly threadId?: string;
|
|
1046
|
+
readonly depth?: number;
|
|
1047
|
+
readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
|
|
589
1048
|
}): Promise<{
|
|
590
1049
|
readonly output: unknown;
|
|
591
1050
|
readonly text: string;
|
|
@@ -595,6 +1054,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
595
1054
|
readonly denials: AgentDenial[];
|
|
596
1055
|
readonly steps: number;
|
|
597
1056
|
readonly cost: number;
|
|
1057
|
+
readonly budgetExceeded: boolean;
|
|
1058
|
+
readonly stopReason: AgentStopReason;
|
|
598
1059
|
readonly inputTokens?: number;
|
|
599
1060
|
readonly outputTokens?: number;
|
|
600
1061
|
}> {
|
|
@@ -609,20 +1070,86 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
609
1070
|
const runDenials: AgentDenial[] = [];
|
|
610
1071
|
let steps = 0;
|
|
611
1072
|
let cost = 0;
|
|
1073
|
+
let budgetExceeded = false;
|
|
1074
|
+
let stopReason: AgentStopReason = "max_steps";
|
|
612
1075
|
const tokens: { inputTokens?: number; outputTokens?: number } = {};
|
|
613
1076
|
let lastText = "";
|
|
614
1077
|
let lastRaw: unknown = {};
|
|
615
1078
|
let lastToolResult: unknown;
|
|
1079
|
+
let messageSeq = 0;
|
|
1080
|
+
|
|
1081
|
+
const capHit = (): boolean => opts.maxCostPerRun !== undefined && cost >= opts.maxCostPerRun;
|
|
1082
|
+
|
|
1083
|
+
const finish = () => ({
|
|
1084
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1085
|
+
text: lastText,
|
|
1086
|
+
raw: lastRaw,
|
|
1087
|
+
lastToolResult,
|
|
1088
|
+
trail,
|
|
1089
|
+
denials: runDenials,
|
|
1090
|
+
steps,
|
|
1091
|
+
cost,
|
|
1092
|
+
budgetExceeded,
|
|
1093
|
+
stopReason,
|
|
1094
|
+
...tokenFields(tokens),
|
|
1095
|
+
});
|
|
616
1096
|
|
|
617
1097
|
const providerModel = wireModel(opts.modelName, opts.client);
|
|
618
1098
|
while (steps < opts.maxSteps) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1099
|
+
if (opts.signal?.aborted) {
|
|
1100
|
+
stopReason = "aborted";
|
|
1101
|
+
throw new AgentLoopHalt("aborted", new Error("aborted"), {
|
|
1102
|
+
trail,
|
|
1103
|
+
denials: runDenials,
|
|
1104
|
+
steps,
|
|
1105
|
+
cost,
|
|
1106
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (capHit()) {
|
|
1110
|
+
budgetExceeded = true;
|
|
1111
|
+
stopReason = "budget";
|
|
1112
|
+
return finish();
|
|
1113
|
+
}
|
|
1114
|
+
const stepName = `step-${steps + 1}`;
|
|
1115
|
+
opts.emit?.({ type: "STEP_STARTED", stepName });
|
|
1116
|
+
let result: Awaited<ReturnType<AiModelClient["complete"]>>;
|
|
1117
|
+
let streamedLive = false;
|
|
1118
|
+
let executed = false;
|
|
1119
|
+
try {
|
|
1120
|
+
const produce = async () => {
|
|
1121
|
+
executed = true;
|
|
1122
|
+
const turn = await loadStreamTurn().readModelTurn(
|
|
1123
|
+
opts.client,
|
|
1124
|
+
{
|
|
1125
|
+
model: providerModel,
|
|
1126
|
+
messages,
|
|
1127
|
+
tools: defs.length > 0 ? defs : undefined,
|
|
1128
|
+
responseFormat: opts.responseFormat,
|
|
1129
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
1130
|
+
},
|
|
1131
|
+
opts.emit,
|
|
1132
|
+
() => `m-${++messageSeq}`,
|
|
1133
|
+
);
|
|
1134
|
+
streamedLive = turn.streamed;
|
|
1135
|
+
return turn.result;
|
|
1136
|
+
};
|
|
1137
|
+
result = opts.journal
|
|
1138
|
+
? await opts.journal.effect("ask", `${opts.agentLabel}:${steps}`, produce)
|
|
1139
|
+
: await produce();
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
if (err instanceof AgentLoopHalt) throw err;
|
|
1142
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1143
|
+
throw new AgentLoopHalt("aborted", err, {
|
|
1144
|
+
trail,
|
|
1145
|
+
denials: runDenials,
|
|
1146
|
+
steps,
|
|
1147
|
+
cost,
|
|
1148
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
throw err;
|
|
1152
|
+
}
|
|
626
1153
|
if (result.external !== undefined) {
|
|
627
1154
|
egress.lastExternal = result.external;
|
|
628
1155
|
}
|
|
@@ -630,20 +1157,24 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
630
1157
|
addUsageTokens(tokens, result.usage);
|
|
631
1158
|
lastText = result.text;
|
|
632
1159
|
lastRaw = result.raw !== undefined ? result.raw : result.text;
|
|
1160
|
+
const replayed = opts.journal !== undefined && !executed;
|
|
1161
|
+
const messageId = streamedLive || replayed ? "" : `m-${++messageSeq}`;
|
|
1162
|
+
const emittedText =
|
|
1163
|
+
streamedLive || replayed
|
|
1164
|
+
? false
|
|
1165
|
+
: emitAssistantText(opts.emit ?? (() => undefined), messageId, result.text);
|
|
1166
|
+
if (capHit()) {
|
|
1167
|
+
budgetExceeded = true;
|
|
1168
|
+
stopReason = "budget";
|
|
1169
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
1170
|
+
return finish();
|
|
1171
|
+
}
|
|
633
1172
|
|
|
634
1173
|
const toolCalls = result.toolCalls;
|
|
635
1174
|
if (!toolCalls || toolCalls.length === 0) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
raw: lastRaw,
|
|
640
|
-
lastToolResult,
|
|
641
|
-
trail,
|
|
642
|
-
denials: runDenials,
|
|
643
|
-
steps,
|
|
644
|
-
cost,
|
|
645
|
-
...tokenFields(tokens),
|
|
646
|
-
};
|
|
1175
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
1176
|
+
stopReason = "completed";
|
|
1177
|
+
return finish();
|
|
647
1178
|
}
|
|
648
1179
|
|
|
649
1180
|
messages.push({
|
|
@@ -652,43 +1183,111 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
652
1183
|
toolCalls,
|
|
653
1184
|
});
|
|
654
1185
|
|
|
655
|
-
for (const tc of toolCalls) {
|
|
1186
|
+
for (const [index, tc] of toolCalls.entries()) {
|
|
656
1187
|
if (steps >= opts.maxSteps) break;
|
|
657
1188
|
steps++;
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1189
|
+
const callId = tc.id.length > 0 ? tc.id : `${opts.agentLabel}:${steps}:${index}`;
|
|
1190
|
+
if (!streamedLive && !replayed) {
|
|
1191
|
+
opts.emit?.({
|
|
1192
|
+
type: "TOOL_CALL_START",
|
|
1193
|
+
toolCallId: callId,
|
|
1194
|
+
toolCallName: tc.name,
|
|
1195
|
+
...(emittedText ? { parentMessageId: messageId } : {}),
|
|
1196
|
+
});
|
|
1197
|
+
opts.emit?.({
|
|
1198
|
+
type: "TOOL_CALL_ARGS",
|
|
1199
|
+
toolCallId: callId,
|
|
1200
|
+
delta: JSON.stringify(tc.arguments ?? {}),
|
|
1201
|
+
});
|
|
1202
|
+
opts.emit?.({ type: "TOOL_CALL_END", toolCallId: callId });
|
|
1203
|
+
}
|
|
1204
|
+
let toolResult: unknown;
|
|
1205
|
+
const spend = { cost: 0, inputTokens: 0, outputTokens: 0 };
|
|
1206
|
+
try {
|
|
1207
|
+
toolResult = await dispatchTool({
|
|
1208
|
+
tool: tc.name,
|
|
1209
|
+
args: tc.arguments,
|
|
1210
|
+
agentLabel: opts.agentLabel,
|
|
1211
|
+
allowedTools: allowed,
|
|
1212
|
+
callTool: opts.callTool,
|
|
1213
|
+
auth: opts.auth,
|
|
1214
|
+
operator: opts.operator,
|
|
1215
|
+
meta: opts.meta,
|
|
1216
|
+
trail,
|
|
1217
|
+
runDenials,
|
|
1218
|
+
callId,
|
|
1219
|
+
step: steps,
|
|
1220
|
+
index,
|
|
1221
|
+
...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
|
|
1222
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
1223
|
+
...(opts.journal !== undefined ? { journal: opts.journal } : {}),
|
|
1224
|
+
...(opts.flow !== undefined ? { flow: opts.flow } : {}),
|
|
1225
|
+
...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
|
|
1226
|
+
...(opts.approvals !== undefined ? { approvals: opts.approvals } : {}),
|
|
1227
|
+
...(opts.emit !== undefined ? { emit: opts.emit } : {}),
|
|
1228
|
+
...(opts.runId !== undefined ? { runId: opts.runId } : {}),
|
|
1229
|
+
...(opts.depth !== undefined ? { depth: opts.depth } : {}),
|
|
1230
|
+
...(opts.maxCostPerRun !== undefined ? { maxCostPerRun: opts.maxCostPerRun } : {}),
|
|
1231
|
+
spent: cost,
|
|
1232
|
+
...(opts.recordCall !== undefined ? { recordCall: opts.recordCall } : {}),
|
|
1233
|
+
spend,
|
|
1234
|
+
});
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
if (err instanceof AgentLoopHalt || isJournalSuspend(err)) throw err;
|
|
1237
|
+
if (err instanceof AiDurableRequiredError) throw err;
|
|
1238
|
+
if (err instanceof Error && err.name === "AgentDenied") {
|
|
1239
|
+
throw new AgentLoopHalt("denied", err, {
|
|
1240
|
+
trail,
|
|
1241
|
+
denials: runDenials,
|
|
1242
|
+
steps,
|
|
1243
|
+
cost,
|
|
1244
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1248
|
+
throw new AgentLoopHalt("aborted", err, {
|
|
1249
|
+
trail,
|
|
1250
|
+
denials: runDenials,
|
|
1251
|
+
steps,
|
|
1252
|
+
cost,
|
|
1253
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
throw new AgentLoopHalt("error", err, {
|
|
1257
|
+
trail,
|
|
1258
|
+
denials: runDenials,
|
|
1259
|
+
steps,
|
|
1260
|
+
cost,
|
|
1261
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
671
1264
|
lastToolResult = toolResult;
|
|
1265
|
+
cost += spend.cost;
|
|
1266
|
+
addUsageTokens(tokens, spend);
|
|
1267
|
+
opts.emit?.({
|
|
1268
|
+
type: "TOOL_CALL_RESULT",
|
|
1269
|
+
messageId: `m-${++messageSeq}`,
|
|
1270
|
+
toolCallId: callId,
|
|
1271
|
+
content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult ?? null),
|
|
1272
|
+
role: "tool",
|
|
1273
|
+
});
|
|
672
1274
|
messages.push({
|
|
673
1275
|
role: "tool",
|
|
674
1276
|
content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult ?? null),
|
|
675
|
-
toolCallId:
|
|
1277
|
+
toolCallId: callId,
|
|
676
1278
|
name: tc.name,
|
|
677
1279
|
});
|
|
1280
|
+
if (capHit()) {
|
|
1281
|
+
budgetExceeded = true;
|
|
1282
|
+
stopReason = "budget";
|
|
1283
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
1284
|
+
return finish();
|
|
1285
|
+
}
|
|
678
1286
|
}
|
|
1287
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
679
1288
|
}
|
|
680
1289
|
|
|
681
|
-
return
|
|
682
|
-
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
683
|
-
text: lastText,
|
|
684
|
-
raw: lastRaw,
|
|
685
|
-
lastToolResult,
|
|
686
|
-
trail,
|
|
687
|
-
denials: runDenials,
|
|
688
|
-
steps,
|
|
689
|
-
cost,
|
|
690
|
-
...tokenFields(tokens),
|
|
691
|
-
};
|
|
1290
|
+
return finish();
|
|
692
1291
|
}
|
|
693
1292
|
|
|
694
1293
|
const runtime: AiRuntime = {
|
|
@@ -721,21 +1320,6 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
721
1320
|
currentAbortSignal(),
|
|
722
1321
|
);
|
|
723
1322
|
|
|
724
|
-
// Replay from journal when input matches (nondeterministic contract)
|
|
725
|
-
if (journalingForced && tools.length === 0) {
|
|
726
|
-
const hit = [...journal]
|
|
727
|
-
.reverse()
|
|
728
|
-
.find(
|
|
729
|
-
(e) =>
|
|
730
|
-
e.prompt === prompt &&
|
|
731
|
-
e.outcome === "ok" &&
|
|
732
|
-
JSON.stringify(e.input) === JSON.stringify(input),
|
|
733
|
-
);
|
|
734
|
-
if (hit) {
|
|
735
|
-
return hit.output as Record<string, unknown>;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
1323
|
const via =
|
|
740
1324
|
opts?.via ?? decl.via ?? (decl.model ? [decl.model] : [...models.keys()].slice(0, 1));
|
|
741
1325
|
const attempts: AiFallbackAttempt[] = [];
|
|
@@ -747,7 +1331,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
747
1331
|
const responseFormat = promptResponseFormat(prompt, decl.out);
|
|
748
1332
|
|
|
749
1333
|
const pushJournal = (entry: Omit<AiJournalEntry, "inputTokens" | "outputTokens">): void => {
|
|
750
|
-
journal
|
|
1334
|
+
pushObservability(journal, {
|
|
751
1335
|
...entry,
|
|
752
1336
|
...tokenFields(totalTokens),
|
|
753
1337
|
});
|
|
@@ -777,6 +1361,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
777
1361
|
|
|
778
1362
|
for (const modelName of via) {
|
|
779
1363
|
let sameModelTries = 0;
|
|
1364
|
+
let repaired = false;
|
|
780
1365
|
let advance = true;
|
|
781
1366
|
while (sameModelTries < 2 && advance) {
|
|
782
1367
|
sameModelTries++;
|
|
@@ -785,6 +1370,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
785
1370
|
const client = await clientFor(modelName);
|
|
786
1371
|
let raw: unknown;
|
|
787
1372
|
let attemptCost = 0;
|
|
1373
|
+
const sent: AiMessage[] = [{ role: "user", content: userContent }];
|
|
788
1374
|
|
|
789
1375
|
if (tools.length > 0) {
|
|
790
1376
|
const loop = await toolLoop({
|
|
@@ -812,7 +1398,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
812
1398
|
} else {
|
|
813
1399
|
const result = await client.complete({
|
|
814
1400
|
model: wireModel(modelName, client),
|
|
815
|
-
messages:
|
|
1401
|
+
messages: sent,
|
|
816
1402
|
...(responseFormat !== undefined ? { responseFormat } : {}),
|
|
817
1403
|
...(signal !== undefined ? { signal } : {}),
|
|
818
1404
|
});
|
|
@@ -888,12 +1474,83 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
888
1474
|
at: now(),
|
|
889
1475
|
});
|
|
890
1476
|
}
|
|
1477
|
+
if (decl.repair === 1 && !repaired) {
|
|
1478
|
+
repaired = true;
|
|
1479
|
+
const follow = await client.complete({
|
|
1480
|
+
model: wireModel(modelName, client),
|
|
1481
|
+
messages: [
|
|
1482
|
+
...sent,
|
|
1483
|
+
{
|
|
1484
|
+
role: "user",
|
|
1485
|
+
content: `Schema mismatch: ${err.message}. Reply with JSON only.`,
|
|
1486
|
+
},
|
|
1487
|
+
],
|
|
1488
|
+
...(responseFormat !== undefined ? { responseFormat } : {}),
|
|
1489
|
+
...(signal !== undefined ? { signal } : {}),
|
|
1490
|
+
});
|
|
1491
|
+
const repairCost = follow.usage?.cost ?? 0;
|
|
1492
|
+
totalCost += repairCost;
|
|
1493
|
+
addUsageTokens(totalTokens, follow.usage);
|
|
1494
|
+
attempts.push({
|
|
1495
|
+
model: modelName,
|
|
1496
|
+
ok: true,
|
|
1497
|
+
cost: repairCost,
|
|
1498
|
+
latencyMs: Math.max(0, now() - attemptStart),
|
|
1499
|
+
at: now(),
|
|
1500
|
+
});
|
|
1501
|
+
assertAskBudget(totalCost);
|
|
1502
|
+
let repairRaw: unknown =
|
|
1503
|
+
typeof follow.text === "string" && follow.text.length > 0
|
|
1504
|
+
? follow.text
|
|
1505
|
+
: follow.raw !== undefined
|
|
1506
|
+
? follow.raw
|
|
1507
|
+
: follow.text;
|
|
1508
|
+
try {
|
|
1509
|
+
const coerced = coerceModelObject(repairRaw);
|
|
1510
|
+
const prepared = outExpectsVia(decl.out)
|
|
1511
|
+
? { ...coerced, via: modelName }
|
|
1512
|
+
: coerced;
|
|
1513
|
+
const validated = validatePromptOut(prompt, version, decl.out, prepared);
|
|
1514
|
+
const output = { ...validated, via: modelName };
|
|
1515
|
+
if (journalingForced) {
|
|
1516
|
+
pushJournal({
|
|
1517
|
+
prompt,
|
|
1518
|
+
...(version !== undefined ? { version } : {}),
|
|
1519
|
+
input,
|
|
1520
|
+
output,
|
|
1521
|
+
attempts: [...attempts],
|
|
1522
|
+
outcome: "ok",
|
|
1523
|
+
cost: totalCost,
|
|
1524
|
+
latencyMs: Math.max(0, now() - started),
|
|
1525
|
+
at: now(),
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
return output;
|
|
1529
|
+
} catch (again) {
|
|
1530
|
+
if (again instanceof AiSchemaValidationError && journalingForced) {
|
|
1531
|
+
pushJournal({
|
|
1532
|
+
prompt,
|
|
1533
|
+
...(version !== undefined ? { version } : {}),
|
|
1534
|
+
input,
|
|
1535
|
+
output: coerceModelObject(repairRaw),
|
|
1536
|
+
attempts: [...attempts],
|
|
1537
|
+
outcome: "schema_invalid",
|
|
1538
|
+
cost: totalCost,
|
|
1539
|
+
latencyMs: Math.max(0, now() - started),
|
|
1540
|
+
schemaMismatch: again.mismatch,
|
|
1541
|
+
at: now(),
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
throw again;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
891
1547
|
throw err;
|
|
892
1548
|
}
|
|
893
1549
|
throw err;
|
|
894
1550
|
}
|
|
895
1551
|
} catch (err) {
|
|
896
1552
|
if (err instanceof AiSchemaValidationError) throw err;
|
|
1553
|
+
if (err instanceof Error && err.name === "AiBudgetExceededError") throw err;
|
|
897
1554
|
lastError = err instanceof Error ? err.message : String(err);
|
|
898
1555
|
attempts.push({
|
|
899
1556
|
model: modelName,
|
|
@@ -955,49 +1612,456 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
955
1612
|
const modelName = decl.model ?? [...models.keys()][0] ?? "mock";
|
|
956
1613
|
const client = await clientFor(modelName);
|
|
957
1614
|
const started = now();
|
|
1615
|
+
const runId = await allocateAgentRunId(agent, runOpts);
|
|
1616
|
+
runOpts.onRunId?.(runId);
|
|
1617
|
+
const threadId = runOpts.threadId ?? loadOkid().okid();
|
|
1618
|
+
const logHeader = agentLogHeader(runId, threadId, agent, runOpts);
|
|
1619
|
+
const safeAppend = async (event: AgUiEvent): Promise<void> => {
|
|
1620
|
+
try {
|
|
1621
|
+
await eventLog.append(runId, event, now());
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
if (runOpts.journal) {
|
|
1627
|
+
await eventLog.open(logHeader);
|
|
1628
|
+
await safeAppend({ type: "RUN_STARTED", threadId, runId });
|
|
1629
|
+
}
|
|
958
1630
|
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1631
|
+
const remember = (partial: {
|
|
1632
|
+
readonly ok: boolean;
|
|
1633
|
+
readonly stopReason: AgentStopReason;
|
|
1634
|
+
readonly steps: number;
|
|
1635
|
+
readonly trail: readonly AgentToolStep[];
|
|
1636
|
+
readonly denials: readonly AgentDenial[];
|
|
1637
|
+
readonly output: unknown;
|
|
1638
|
+
readonly cost: number;
|
|
1639
|
+
readonly error?: string;
|
|
1640
|
+
}) => {
|
|
1641
|
+
const record: AgentRunRecord = {
|
|
1642
|
+
id: runId,
|
|
1643
|
+
agent,
|
|
1644
|
+
message: agentMessageLabel(runOpts),
|
|
1645
|
+
...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
|
|
1646
|
+
ok: partial.ok,
|
|
1647
|
+
stopReason: partial.stopReason,
|
|
1648
|
+
...(partial.error !== undefined ? { error: partial.error } : {}),
|
|
1649
|
+
steps: partial.steps,
|
|
1650
|
+
trail: partial.trail,
|
|
1651
|
+
denials: partial.denials,
|
|
1652
|
+
output: partial.output,
|
|
1653
|
+
at: started,
|
|
1654
|
+
finishedAt: now(),
|
|
1655
|
+
threadId,
|
|
1656
|
+
cost: partial.cost,
|
|
1657
|
+
...(loopTokens.inputTokens !== undefined ? { inputTokens: loopTokens.inputTokens } : {}),
|
|
1658
|
+
...(loopTokens.outputTokens !== undefined
|
|
1659
|
+
? { outputTokens: loopTokens.outputTokens }
|
|
1660
|
+
: {}),
|
|
1661
|
+
};
|
|
1662
|
+
pushObservability(agentRuns, record);
|
|
1663
|
+
return {
|
|
1664
|
+
runId,
|
|
1665
|
+
ok: record.ok,
|
|
1666
|
+
stopReason: record.stopReason,
|
|
1667
|
+
steps: record.steps,
|
|
1668
|
+
denials: record.denials,
|
|
1669
|
+
trail: record.trail,
|
|
1670
|
+
output: record.output,
|
|
1671
|
+
cost: record.cost,
|
|
1672
|
+
...(loopTokens.inputTokens !== undefined ? { inputTokens: loopTokens.inputTokens } : {}),
|
|
1673
|
+
...(loopTokens.outputTokens !== undefined
|
|
1674
|
+
? { outputTokens: loopTokens.outputTokens }
|
|
1675
|
+
: {}),
|
|
1676
|
+
};
|
|
1677
|
+
};
|
|
1678
|
+
let loopTokens: { inputTokens?: number; outputTokens?: number } = {};
|
|
1679
|
+
let appendChain: Promise<unknown> = Promise.resolve();
|
|
1680
|
+
|
|
1681
|
+
try {
|
|
1682
|
+
const loop = await toolLoop({
|
|
1683
|
+
client,
|
|
1684
|
+
modelName,
|
|
1685
|
+
messages: agentMessages(runOpts),
|
|
1686
|
+
tools: decl.tools,
|
|
1687
|
+
maxSteps,
|
|
1688
|
+
...(runOpts.maxCostPerRun !== undefined
|
|
1689
|
+
? { maxCostPerRun: runOpts.maxCostPerRun }
|
|
1690
|
+
: decl.budget?.maxCostPerRun !== undefined
|
|
1691
|
+
? { maxCostPerRun: decl.budget.maxCostPerRun }
|
|
1692
|
+
: {}),
|
|
1693
|
+
agentLabel: agent,
|
|
1694
|
+
runId,
|
|
1695
|
+
depth: runOpts.depth ?? 1,
|
|
1696
|
+
...(runOpts.recordCall !== undefined ? { recordCall: runOpts.recordCall } : {}),
|
|
1697
|
+
callTool: runOpts.callTool,
|
|
1698
|
+
auth: runOpts.auth,
|
|
1699
|
+
operator: runOpts.operator,
|
|
1700
|
+
meta: runOpts.meta,
|
|
1701
|
+
signal: currentAbortSignal(),
|
|
1702
|
+
threadId,
|
|
1703
|
+
...(runOpts.journal !== undefined
|
|
1704
|
+
? {
|
|
1705
|
+
emit: (event: AgUiEvent) => {
|
|
1706
|
+
appendChain = appendChain.then(() => safeAppend(event));
|
|
1707
|
+
},
|
|
1708
|
+
}
|
|
1709
|
+
: {}),
|
|
1710
|
+
...(runOpts.journal !== undefined ? { journal: runOpts.journal } : {}),
|
|
1711
|
+
...(runOpts.flow !== undefined ? { flow: runOpts.flow } : {}),
|
|
1712
|
+
...(runOpts.tenantId !== undefined ? { tenantId: runOpts.tenantId } : {}),
|
|
1713
|
+
...(decl.approvals !== undefined ? { approvals: decl.approvals } : {}),
|
|
1714
|
+
});
|
|
1715
|
+
await appendChain;
|
|
1716
|
+
loopTokens = tokenFields(loop);
|
|
1717
|
+
const settled = remember({
|
|
1718
|
+
ok: loop.stopReason === "completed" && loop.denials.length === 0,
|
|
1719
|
+
stopReason: loop.stopReason,
|
|
1720
|
+
steps: loop.steps,
|
|
1721
|
+
trail: loop.trail,
|
|
1722
|
+
denials: loop.denials,
|
|
1723
|
+
output: loop.output,
|
|
1724
|
+
cost: loop.cost,
|
|
1725
|
+
});
|
|
1726
|
+
if (runOpts.journal) {
|
|
1727
|
+
await safeAppend({
|
|
1728
|
+
type: "RUN_FINISHED",
|
|
1729
|
+
threadId,
|
|
1730
|
+
runId,
|
|
1731
|
+
result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
|
|
1732
|
+
...(loopTokens.inputTokens !== undefined || loopTokens.outputTokens !== undefined
|
|
1733
|
+
? { usage: [tokenFields(loopTokens)] }
|
|
1734
|
+
: {}),
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
return settled;
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
if (err instanceof AgentLoopHalt) {
|
|
1740
|
+
const result = remember({
|
|
1741
|
+
ok: false,
|
|
1742
|
+
stopReason: err.stopReason,
|
|
1743
|
+
steps: err.steps,
|
|
1744
|
+
trail: err.trail,
|
|
1745
|
+
denials: err.denials,
|
|
1746
|
+
output: err.output,
|
|
1747
|
+
cost: err.cost,
|
|
1748
|
+
...(err.stopReason === "error"
|
|
1749
|
+
? { error: err.cause instanceof Error ? err.cause.message : String(err.cause) }
|
|
1750
|
+
: {}),
|
|
1751
|
+
});
|
|
1752
|
+
if (err.stopReason === "aborted" || err.stopReason === "error") {
|
|
1753
|
+
if (runOpts.journal) {
|
|
1754
|
+
const cause = err.cause instanceof Error ? err.cause : undefined;
|
|
1755
|
+
await safeAppend({
|
|
1756
|
+
type: "RUN_ERROR",
|
|
1757
|
+
message: cause?.message ?? String(err.cause),
|
|
1758
|
+
...(cause && cause.name !== "Error" ? { code: cause.name } : {}),
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
throw err.cause;
|
|
1762
|
+
}
|
|
1763
|
+
if (runOpts.journal) {
|
|
1764
|
+
await safeAppend({
|
|
1765
|
+
type: "RUN_FINISHED",
|
|
1766
|
+
threadId,
|
|
1767
|
+
runId,
|
|
1768
|
+
result: {
|
|
1769
|
+
cost: err.cost,
|
|
1770
|
+
stopReason: err.stopReason,
|
|
1771
|
+
output: err.output,
|
|
1772
|
+
},
|
|
1773
|
+
...(loopTokens.inputTokens !== undefined || loopTokens.outputTokens !== undefined
|
|
1774
|
+
? { usage: [tokenFields(loopTokens)] }
|
|
1775
|
+
: {}),
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
return result;
|
|
1779
|
+
}
|
|
1780
|
+
if (runOpts.journal) {
|
|
1781
|
+
const error = err instanceof Error ? err : undefined;
|
|
1782
|
+
await safeAppend({
|
|
1783
|
+
type: "RUN_ERROR",
|
|
1784
|
+
message: error?.message ?? String(err),
|
|
1785
|
+
...(error && error.name !== "Error" ? { code: error.name } : {}),
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
976
1788
|
throw err;
|
|
977
1789
|
}
|
|
1790
|
+
},
|
|
978
1791
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1792
|
+
streamAsk(prompt, input, opts) {
|
|
1793
|
+
const queue = createEventQueue();
|
|
1794
|
+
const runId = `ask-${++runSeq}`;
|
|
1795
|
+
const signal = currentAbortSignal();
|
|
1796
|
+
void (async () => {
|
|
1797
|
+
try {
|
|
1798
|
+
queue.emit({ type: "RUN_STARTED", threadId: "default", runId });
|
|
1799
|
+
const tools = opts?.tools ?? [];
|
|
1800
|
+
if (tools.length > 0) {
|
|
1801
|
+
const pin = parsePromptRef(prompt);
|
|
1802
|
+
const decl = prompts.get(pin.name) ?? prompts.get(prompt);
|
|
1803
|
+
if (!decl) throw new Error(`ai: unknown prompt "${prompt}"`);
|
|
1804
|
+
const via =
|
|
1805
|
+
opts?.via ?? decl.via ?? (decl.model ? [decl.model] : [...models.keys()].slice(0, 1));
|
|
1806
|
+
const modelName = via[0];
|
|
1807
|
+
if (!modelName) throw new Error(`ai: no model for prompt "${prompt}"`);
|
|
1808
|
+
const client = await clientFor(modelName);
|
|
1809
|
+
const loop = await toolLoop({
|
|
1810
|
+
client,
|
|
1811
|
+
modelName,
|
|
1812
|
+
messages: [{ role: "user", content: askUserContent(input, decl.out) }],
|
|
1813
|
+
tools,
|
|
1814
|
+
maxSteps: opts?.maxSteps ?? AI_DEFAULT_MAX_STEPS,
|
|
1815
|
+
agentLabel: prompt,
|
|
1816
|
+
callTool: opts?.callTool,
|
|
1817
|
+
emit: queue.emit,
|
|
1818
|
+
signal,
|
|
1819
|
+
});
|
|
1820
|
+
queue.emit({
|
|
1821
|
+
type: "RUN_FINISHED",
|
|
1822
|
+
threadId: "default",
|
|
1823
|
+
runId,
|
|
1824
|
+
result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
|
|
1825
|
+
...(tokenFields(loop).inputTokens !== undefined ||
|
|
1826
|
+
tokenFields(loop).outputTokens !== undefined
|
|
1827
|
+
? { usage: [tokenFields(loop)] }
|
|
1828
|
+
: {}),
|
|
1829
|
+
});
|
|
1830
|
+
} else {
|
|
1831
|
+
const output = await runtime.ask(prompt, input, opts);
|
|
1832
|
+
emitAssistantText(queue.emit, "m-1", JSON.stringify(output));
|
|
1833
|
+
const spent = journal.at(-1)?.cost ?? 0;
|
|
1834
|
+
queue.emit({
|
|
1835
|
+
type: "RUN_FINISHED",
|
|
1836
|
+
threadId: "default",
|
|
1837
|
+
runId,
|
|
1838
|
+
result: { cost: spent, stopReason: "completed", output },
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
queue.finish();
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
queue.emit({
|
|
1844
|
+
type: "RUN_ERROR",
|
|
1845
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1846
|
+
...(err instanceof Error && err.name !== "Error" ? { code: err.name } : {}),
|
|
1847
|
+
});
|
|
1848
|
+
queue.finish();
|
|
1849
|
+
}
|
|
1850
|
+
})();
|
|
1851
|
+
return queue.events;
|
|
1852
|
+
},
|
|
1853
|
+
|
|
1854
|
+
streamAgent(agent, runOpts) {
|
|
1855
|
+
const queue = createEventQueue();
|
|
1856
|
+
const signal = currentAbortSignal();
|
|
1857
|
+
const threadId = runOpts.threadId ?? loadOkid().okid();
|
|
1858
|
+
let skipLoggedStart = false;
|
|
1859
|
+
let runId = runOpts.runId ?? "";
|
|
1860
|
+
let appendChain: Promise<unknown> = Promise.resolve();
|
|
1861
|
+
const emit: AgentEventEmit = (event) => {
|
|
1862
|
+
if (!runOpts.journal) {
|
|
1863
|
+
queue.emit(event);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (skipLoggedStart && event.type === "RUN_STARTED") {
|
|
1867
|
+
queue.emit(event);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
appendChain = appendChain.then(async () => {
|
|
1871
|
+
let seq: number | undefined;
|
|
1872
|
+
try {
|
|
1873
|
+
seq = await eventLog.append(runId, event, now());
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
|
|
1876
|
+
}
|
|
1877
|
+
queue.emit(seq !== undefined ? loadSseId().withSseId(event, String(seq)) : event);
|
|
1878
|
+
});
|
|
1000
1879
|
};
|
|
1880
|
+
let resolveResult: (value: unknown) => void = () => undefined;
|
|
1881
|
+
let rejectResult: (err: unknown) => void = () => undefined;
|
|
1882
|
+
const result = new Promise<unknown>((resolve, reject) => {
|
|
1883
|
+
resolveResult = resolve;
|
|
1884
|
+
rejectResult = reject;
|
|
1885
|
+
});
|
|
1886
|
+
// HTTP drains the iterator and may never await `result`.
|
|
1887
|
+
void result.catch(() => undefined);
|
|
1888
|
+
void (async () => {
|
|
1889
|
+
let closed = false;
|
|
1890
|
+
const close = (err?: unknown): void => {
|
|
1891
|
+
if (closed) return;
|
|
1892
|
+
closed = true;
|
|
1893
|
+
queue.finish(err);
|
|
1894
|
+
};
|
|
1895
|
+
try {
|
|
1896
|
+
if (runOpts.journal) {
|
|
1897
|
+
runId = await allocateAgentRunId(agent, runOpts);
|
|
1898
|
+
runOpts.onRunId?.(runId);
|
|
1899
|
+
await eventLog.open(agentLogHeader(runId, threadId, agent, runOpts));
|
|
1900
|
+
const prior = await eventLog.read(runId, 0);
|
|
1901
|
+
skipLoggedStart = prior.some((row) => row.event.type === "RUN_STARTED");
|
|
1902
|
+
} else {
|
|
1903
|
+
runId = runOpts.runId ?? loadOkid().okid();
|
|
1904
|
+
runOpts.onRunId?.(runId);
|
|
1905
|
+
}
|
|
1906
|
+
emit({ type: "RUN_STARTED", threadId, runId });
|
|
1907
|
+
const decl = agents.get(agent);
|
|
1908
|
+
if (!decl) throw new Error(`ai: unknown agent "${agent}"`);
|
|
1909
|
+
const maxSteps = decl.maxSteps ?? AI_DEFAULT_MAX_STEPS;
|
|
1910
|
+
const modelName = decl.model ?? [...models.keys()][0] ?? "mock";
|
|
1911
|
+
const client = await clientFor(modelName);
|
|
1912
|
+
const started = now();
|
|
1913
|
+
const loop = await toolLoop({
|
|
1914
|
+
client,
|
|
1915
|
+
modelName,
|
|
1916
|
+
messages: agentMessages(runOpts),
|
|
1917
|
+
tools: decl.tools,
|
|
1918
|
+
maxSteps,
|
|
1919
|
+
...(runOpts.maxCostPerRun !== undefined
|
|
1920
|
+
? { maxCostPerRun: runOpts.maxCostPerRun }
|
|
1921
|
+
: decl.budget?.maxCostPerRun !== undefined
|
|
1922
|
+
? { maxCostPerRun: decl.budget.maxCostPerRun }
|
|
1923
|
+
: {}),
|
|
1924
|
+
agentLabel: agent,
|
|
1925
|
+
runId,
|
|
1926
|
+
depth: runOpts.depth ?? 1,
|
|
1927
|
+
...(runOpts.recordCall !== undefined ? { recordCall: runOpts.recordCall } : {}),
|
|
1928
|
+
callTool: runOpts.callTool,
|
|
1929
|
+
auth: runOpts.auth,
|
|
1930
|
+
operator: runOpts.operator,
|
|
1931
|
+
meta: runOpts.meta,
|
|
1932
|
+
emit,
|
|
1933
|
+
signal,
|
|
1934
|
+
threadId,
|
|
1935
|
+
...(runOpts.journal !== undefined ? { journal: runOpts.journal } : {}),
|
|
1936
|
+
...(runOpts.flow !== undefined ? { flow: runOpts.flow } : {}),
|
|
1937
|
+
...(runOpts.tenantId !== undefined ? { tenantId: runOpts.tenantId } : {}),
|
|
1938
|
+
...(decl.approvals !== undefined ? { approvals: decl.approvals } : {}),
|
|
1939
|
+
});
|
|
1940
|
+
const record: AgentRunRecord = {
|
|
1941
|
+
id: runId,
|
|
1942
|
+
agent,
|
|
1943
|
+
message: agentMessageLabel(runOpts),
|
|
1944
|
+
...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
|
|
1945
|
+
ok: loop.stopReason === "completed" && loop.denials.length === 0,
|
|
1946
|
+
stopReason: loop.stopReason,
|
|
1947
|
+
steps: loop.steps,
|
|
1948
|
+
trail: loop.trail,
|
|
1949
|
+
denials: loop.denials,
|
|
1950
|
+
output: loop.output,
|
|
1951
|
+
at: started,
|
|
1952
|
+
cost: loop.cost,
|
|
1953
|
+
};
|
|
1954
|
+
pushObservability(agentRuns, record);
|
|
1955
|
+
const usage = tokenFields(loop);
|
|
1956
|
+
const settled = {
|
|
1957
|
+
ok: record.ok,
|
|
1958
|
+
stopReason: record.stopReason,
|
|
1959
|
+
steps: record.steps,
|
|
1960
|
+
denials: record.denials,
|
|
1961
|
+
trail: record.trail,
|
|
1962
|
+
output: record.output,
|
|
1963
|
+
cost: record.cost,
|
|
1964
|
+
...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
|
|
1965
|
+
...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
|
|
1966
|
+
};
|
|
1967
|
+
resolveResult(settled);
|
|
1968
|
+
await appendChain;
|
|
1969
|
+
emit({
|
|
1970
|
+
type: "RUN_FINISHED",
|
|
1971
|
+
threadId,
|
|
1972
|
+
runId,
|
|
1973
|
+
result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
|
|
1974
|
+
...(usage.inputTokens !== undefined || usage.outputTokens !== undefined
|
|
1975
|
+
? { usage: [usage] }
|
|
1976
|
+
: {}),
|
|
1977
|
+
});
|
|
1978
|
+
await appendChain;
|
|
1979
|
+
close();
|
|
1980
|
+
} catch (err) {
|
|
1981
|
+
if (isJournalSuspend(err)) {
|
|
1982
|
+
await appendChain;
|
|
1983
|
+
rejectResult(err);
|
|
1984
|
+
close(err);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (err instanceof AiDurableRequiredError) {
|
|
1988
|
+
emit({
|
|
1989
|
+
type: "RUN_ERROR",
|
|
1990
|
+
message: err.message,
|
|
1991
|
+
code: err.name,
|
|
1992
|
+
});
|
|
1993
|
+
await appendChain;
|
|
1994
|
+
rejectResult(err);
|
|
1995
|
+
close();
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (err instanceof AgentLoopHalt) {
|
|
1999
|
+
const message = err.cause instanceof Error ? err.cause.message : String(err.cause);
|
|
2000
|
+
pushObservability(agentRuns, {
|
|
2001
|
+
id: runId,
|
|
2002
|
+
agent,
|
|
2003
|
+
message: agentMessageLabel(runOpts),
|
|
2004
|
+
ok: false,
|
|
2005
|
+
stopReason: err.stopReason,
|
|
2006
|
+
...(err.stopReason === "error" ? { error: message } : {}),
|
|
2007
|
+
steps: err.steps,
|
|
2008
|
+
trail: err.trail,
|
|
2009
|
+
denials: err.denials,
|
|
2010
|
+
output: err.output,
|
|
2011
|
+
at: now(),
|
|
2012
|
+
cost: err.cost,
|
|
2013
|
+
});
|
|
2014
|
+
if (err.stopReason === "error") {
|
|
2015
|
+
const settled = {
|
|
2016
|
+
ok: false,
|
|
2017
|
+
stopReason: err.stopReason,
|
|
2018
|
+
error: message,
|
|
2019
|
+
steps: err.steps,
|
|
2020
|
+
denials: err.denials,
|
|
2021
|
+
trail: err.trail,
|
|
2022
|
+
output: err.output,
|
|
2023
|
+
cost: err.cost,
|
|
2024
|
+
};
|
|
2025
|
+
resolveResult(settled);
|
|
2026
|
+
await appendChain;
|
|
2027
|
+
emit({
|
|
2028
|
+
type: "RUN_FINISHED",
|
|
2029
|
+
threadId,
|
|
2030
|
+
runId,
|
|
2031
|
+
result: {
|
|
2032
|
+
cost: err.cost,
|
|
2033
|
+
stopReason: "error",
|
|
2034
|
+
output: err.output,
|
|
2035
|
+
error: message,
|
|
2036
|
+
},
|
|
2037
|
+
});
|
|
2038
|
+
await appendChain;
|
|
2039
|
+
close();
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2044
|
+
rejectResult(err);
|
|
2045
|
+
emit({
|
|
2046
|
+
type: "RUN_ERROR",
|
|
2047
|
+
message,
|
|
2048
|
+
...(err instanceof Error && err.name !== "Error" ? { code: err.name } : {}),
|
|
2049
|
+
});
|
|
2050
|
+
await appendChain;
|
|
2051
|
+
close();
|
|
2052
|
+
} finally {
|
|
2053
|
+
try {
|
|
2054
|
+
await appendChain;
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
|
|
2057
|
+
}
|
|
2058
|
+
if (!closed) {
|
|
2059
|
+
queue.emit({ type: "RUN_ERROR", message: "agent run ended" });
|
|
2060
|
+
close();
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
})();
|
|
2064
|
+
return Object.assign(queue.events, { result });
|
|
1001
2065
|
},
|
|
1002
2066
|
|
|
1003
2067
|
async *stream(model, streamOpts) {
|
|
@@ -1062,6 +2126,23 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
1062
2126
|
await index.upsert(id, vector, { text });
|
|
1063
2127
|
},
|
|
1064
2128
|
|
|
2129
|
+
async resolveApproval(id, decision, ctx) {
|
|
2130
|
+
const store = options.journalStore;
|
|
2131
|
+
if (!store) return { ok: false, status: 404 };
|
|
2132
|
+
const pending = await readAgentApproval(store, id);
|
|
2133
|
+
if (!pending) return { ok: false, status: 404 };
|
|
2134
|
+
if ((decision.tenant ?? null) !== (pending.tenant ?? null)) {
|
|
2135
|
+
return { ok: false, status: 404 };
|
|
2136
|
+
}
|
|
2137
|
+
if (options.gates) {
|
|
2138
|
+
const allowed = await options.gates.allow([pending.gate], ctx);
|
|
2139
|
+
if (!allowed) return { ok: false, status: 403 };
|
|
2140
|
+
} else if (pending.gate !== "public") {
|
|
2141
|
+
return { ok: false, status: 403 };
|
|
2142
|
+
}
|
|
2143
|
+
return resolveAgentApproval(store, id, decision, now);
|
|
2144
|
+
},
|
|
2145
|
+
|
|
1065
2146
|
async embedVector(modelName, text) {
|
|
1066
2147
|
const client = await clientFor(modelName);
|
|
1067
2148
|
if (!client.embed) {
|
|
@@ -1076,6 +2157,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
1076
2157
|
return vector;
|
|
1077
2158
|
},
|
|
1078
2159
|
};
|
|
2160
|
+
self = runtime;
|
|
1079
2161
|
return runtime;
|
|
1080
2162
|
}
|
|
1081
2163
|
|