okengine 0.22.0 → 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 +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +13 -9
- 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/gate/auth.mdx +4 -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/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 +7 -6
- 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/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/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/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-infer.ts +27 -1
- package/src/compiler/extract.ts +268 -12
- 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.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-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.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-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.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-BM27lXld.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 +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.ts +289 -2
- 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/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/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- 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 +103 -1
- 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.ts +15 -4
- 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-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +312 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/journal.ts +104 -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 +29 -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/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,152 @@ 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
|
+
let decision = stored;
|
|
883
|
+
if (decision.status === "pending") {
|
|
884
|
+
opts.emit?.({
|
|
885
|
+
type: "RUN_FINISHED",
|
|
886
|
+
threadId: opts.threadId ?? opts.runId ?? opts.journal.runId,
|
|
887
|
+
runId: opts.runId ?? opts.journal.runId,
|
|
888
|
+
outcome: {
|
|
889
|
+
type: "interrupt",
|
|
890
|
+
interrupts: [{ id, reason: "approval", payload: { tool: capability, args } }],
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
await opts.journal.sleep(approvalStepName(id), approval.timeout, () =>
|
|
894
|
+
approvalTimeoutMs(approval.timeout),
|
|
895
|
+
);
|
|
896
|
+
const store = options.journalStore;
|
|
897
|
+
if (!store) throw new Error("ai: approval resume requires a journal store");
|
|
898
|
+
decision = (await readAgentApproval(store, id)) ?? decision;
|
|
899
|
+
if (decision.status === "pending") {
|
|
900
|
+
const wrote = await resolveAgentApproval(
|
|
901
|
+
store,
|
|
902
|
+
id,
|
|
903
|
+
{ decision: "deny", reason: "timeout", tenant: decision.tenant },
|
|
904
|
+
now,
|
|
905
|
+
opts.journal.run.lockedBy,
|
|
906
|
+
);
|
|
907
|
+
decision = wrote.ok
|
|
908
|
+
? { ...decision, status: "denied", reason: "timeout" }
|
|
909
|
+
: ((await readAgentApproval(store, id)) ?? decision);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (decision.status === "denied") {
|
|
913
|
+
const denial: AgentDenial = {
|
|
914
|
+
agent: agentLabel,
|
|
915
|
+
tool: capability,
|
|
916
|
+
gate: approval.gate,
|
|
917
|
+
reason: decision.reason ?? "denied",
|
|
918
|
+
at: now(),
|
|
919
|
+
};
|
|
920
|
+
runDenials.push(denial);
|
|
921
|
+
denials.push(denial);
|
|
922
|
+
trail.push({ tool: capability, status: "denied", effects, denial, at: denial.at });
|
|
923
|
+
return { denied: true, reason: denial.reason };
|
|
924
|
+
}
|
|
925
|
+
const toolArgs = decision.editedArgs !== undefined ? decision.editedArgs : args;
|
|
926
|
+
const output = await opts.journal.effect("call", approvalStepName(id), () => {
|
|
927
|
+
const call = callTool ?? options.callFlow;
|
|
928
|
+
if (!call) throw new Error("callFlow not configured");
|
|
929
|
+
return withAbortSignal(signal ?? currentAbortSignal(), () => call(capability, toolArgs));
|
|
930
|
+
});
|
|
931
|
+
trail.push({
|
|
932
|
+
tool: capability,
|
|
933
|
+
status: "ok",
|
|
934
|
+
effects,
|
|
935
|
+
at: now(),
|
|
936
|
+
...(decision.approver !== undefined ? { approver: decision.approver } : {}),
|
|
937
|
+
});
|
|
938
|
+
return output;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const childDecl = agents.get(capability) ?? agents.get(tool);
|
|
942
|
+
if (childDecl && self) {
|
|
943
|
+
const depth = opts.depth ?? 1;
|
|
944
|
+
const parentLimit = agents.get(agentLabel)?.maxDepth ?? 3;
|
|
945
|
+
if (depth >= parentLimit) {
|
|
946
|
+
return { error: `ai: agent "${agentLabel}" is nested past maxDepth ${parentLimit}` };
|
|
947
|
+
}
|
|
948
|
+
const childId = loadOkid().okid();
|
|
949
|
+
opts.emit?.({
|
|
950
|
+
type: "CUSTOM",
|
|
951
|
+
name: "oke.subagent.started",
|
|
952
|
+
value: { runId: childId, parentToolCallId: opts.callId },
|
|
953
|
+
});
|
|
954
|
+
await opts.recordCall?.(childDecl.name, childId);
|
|
955
|
+
const parentRemaining =
|
|
956
|
+
opts.maxCostPerRun !== undefined ? opts.maxCostPerRun - (opts.spent ?? 0) : undefined;
|
|
957
|
+
const childCap = childDecl.budget?.maxCostPerRun;
|
|
958
|
+
const cap =
|
|
959
|
+
parentRemaining !== undefined
|
|
960
|
+
? childCap !== undefined
|
|
961
|
+
? Math.min(childCap, parentRemaining)
|
|
962
|
+
: parentRemaining
|
|
963
|
+
: childCap;
|
|
964
|
+
try {
|
|
965
|
+
const child = await self.runAgent(childDecl.name, {
|
|
966
|
+
message: typeof args === "string" ? args : JSON.stringify(args ?? {}),
|
|
967
|
+
runId: childId,
|
|
968
|
+
parentRunId: opts.runId,
|
|
969
|
+
depth: depth + 1,
|
|
970
|
+
...(cap !== undefined ? { maxCostPerRun: cap } : {}),
|
|
971
|
+
...(callTool !== undefined ? { callTool } : {}),
|
|
972
|
+
...(auth !== undefined ? { auth } : {}),
|
|
973
|
+
...(operator !== undefined ? { operator } : {}),
|
|
974
|
+
...(meta !== undefined ? { meta } : {}),
|
|
975
|
+
...(opts.journal !== undefined ? { journal: opts.journal } : {}),
|
|
976
|
+
...(opts.flow !== undefined ? { flow: opts.flow } : {}),
|
|
977
|
+
...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
|
|
978
|
+
...(opts.recordCall !== undefined ? { recordCall: opts.recordCall } : {}),
|
|
979
|
+
});
|
|
980
|
+
if (opts.spend) {
|
|
981
|
+
opts.spend.cost += child.cost;
|
|
982
|
+
opts.spend.inputTokens = (opts.spend.inputTokens ?? 0) + (child.inputTokens ?? 0);
|
|
983
|
+
opts.spend.outputTokens = (opts.spend.outputTokens ?? 0) + (child.outputTokens ?? 0);
|
|
984
|
+
}
|
|
985
|
+
opts.emit?.({
|
|
986
|
+
type: "CUSTOM",
|
|
987
|
+
name: "oke.subagent.finished",
|
|
988
|
+
value: { runId: childId, parentToolCallId: opts.callId },
|
|
989
|
+
});
|
|
990
|
+
trail.push({ tool: capability, status: "ok", effects, at: now() });
|
|
991
|
+
return child.output ?? child;
|
|
992
|
+
} catch (err) {
|
|
993
|
+
opts.emit?.({
|
|
994
|
+
type: "CUSTOM",
|
|
995
|
+
name: "oke.subagent.error",
|
|
996
|
+
value: { runId: childId, parentToolCallId: opts.callId },
|
|
997
|
+
});
|
|
998
|
+
throw err;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
555
1002
|
const invoke = callTool ?? options.callFlow;
|
|
556
1003
|
if (!invoke) {
|
|
557
1004
|
const denial: AgentDenial = {
|
|
@@ -579,6 +1026,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
579
1026
|
readonly messages: AiMessage[];
|
|
580
1027
|
readonly tools: readonly string[];
|
|
581
1028
|
readonly maxSteps: number;
|
|
1029
|
+
/** Stop before the next model call once accumulated cost reaches this cap. */
|
|
1030
|
+
readonly maxCostPerRun?: number;
|
|
582
1031
|
readonly agentLabel: string;
|
|
583
1032
|
readonly responseFormat?: unknown;
|
|
584
1033
|
readonly signal?: AbortSignal;
|
|
@@ -586,6 +1035,15 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
586
1035
|
readonly auth?: GatePolicyContext["auth"];
|
|
587
1036
|
readonly operator?: GatePolicyContext["operator"];
|
|
588
1037
|
readonly meta?: GatePolicyContext["meta"];
|
|
1038
|
+
readonly emit?: AgentEventEmit;
|
|
1039
|
+
readonly journal?: JournalSession;
|
|
1040
|
+
readonly flow?: string;
|
|
1041
|
+
readonly tenantId?: string | null;
|
|
1042
|
+
readonly approvals?: AiAgentDecl["approvals"];
|
|
1043
|
+
readonly runId?: string;
|
|
1044
|
+
readonly threadId?: string;
|
|
1045
|
+
readonly depth?: number;
|
|
1046
|
+
readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
|
|
589
1047
|
}): Promise<{
|
|
590
1048
|
readonly output: unknown;
|
|
591
1049
|
readonly text: string;
|
|
@@ -595,6 +1053,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
595
1053
|
readonly denials: AgentDenial[];
|
|
596
1054
|
readonly steps: number;
|
|
597
1055
|
readonly cost: number;
|
|
1056
|
+
readonly budgetExceeded: boolean;
|
|
1057
|
+
readonly stopReason: AgentStopReason;
|
|
598
1058
|
readonly inputTokens?: number;
|
|
599
1059
|
readonly outputTokens?: number;
|
|
600
1060
|
}> {
|
|
@@ -609,20 +1069,86 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
609
1069
|
const runDenials: AgentDenial[] = [];
|
|
610
1070
|
let steps = 0;
|
|
611
1071
|
let cost = 0;
|
|
1072
|
+
let budgetExceeded = false;
|
|
1073
|
+
let stopReason: AgentStopReason = "max_steps";
|
|
612
1074
|
const tokens: { inputTokens?: number; outputTokens?: number } = {};
|
|
613
1075
|
let lastText = "";
|
|
614
1076
|
let lastRaw: unknown = {};
|
|
615
1077
|
let lastToolResult: unknown;
|
|
1078
|
+
let messageSeq = 0;
|
|
1079
|
+
|
|
1080
|
+
const capHit = (): boolean => opts.maxCostPerRun !== undefined && cost >= opts.maxCostPerRun;
|
|
1081
|
+
|
|
1082
|
+
const finish = () => ({
|
|
1083
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1084
|
+
text: lastText,
|
|
1085
|
+
raw: lastRaw,
|
|
1086
|
+
lastToolResult,
|
|
1087
|
+
trail,
|
|
1088
|
+
denials: runDenials,
|
|
1089
|
+
steps,
|
|
1090
|
+
cost,
|
|
1091
|
+
budgetExceeded,
|
|
1092
|
+
stopReason,
|
|
1093
|
+
...tokenFields(tokens),
|
|
1094
|
+
});
|
|
616
1095
|
|
|
617
1096
|
const providerModel = wireModel(opts.modelName, opts.client);
|
|
618
1097
|
while (steps < opts.maxSteps) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1098
|
+
if (opts.signal?.aborted) {
|
|
1099
|
+
stopReason = "aborted";
|
|
1100
|
+
throw new AgentLoopHalt("aborted", new Error("aborted"), {
|
|
1101
|
+
trail,
|
|
1102
|
+
denials: runDenials,
|
|
1103
|
+
steps,
|
|
1104
|
+
cost,
|
|
1105
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
if (capHit()) {
|
|
1109
|
+
budgetExceeded = true;
|
|
1110
|
+
stopReason = "budget";
|
|
1111
|
+
return finish();
|
|
1112
|
+
}
|
|
1113
|
+
const stepName = `step-${steps + 1}`;
|
|
1114
|
+
opts.emit?.({ type: "STEP_STARTED", stepName });
|
|
1115
|
+
let result: Awaited<ReturnType<AiModelClient["complete"]>>;
|
|
1116
|
+
let streamedLive = false;
|
|
1117
|
+
let executed = false;
|
|
1118
|
+
try {
|
|
1119
|
+
const produce = async () => {
|
|
1120
|
+
executed = true;
|
|
1121
|
+
const turn = await loadStreamTurn().readModelTurn(
|
|
1122
|
+
opts.client,
|
|
1123
|
+
{
|
|
1124
|
+
model: providerModel,
|
|
1125
|
+
messages,
|
|
1126
|
+
tools: defs.length > 0 ? defs : undefined,
|
|
1127
|
+
responseFormat: opts.responseFormat,
|
|
1128
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
1129
|
+
},
|
|
1130
|
+
opts.emit,
|
|
1131
|
+
() => `m-${++messageSeq}`,
|
|
1132
|
+
);
|
|
1133
|
+
streamedLive = turn.streamed;
|
|
1134
|
+
return turn.result;
|
|
1135
|
+
};
|
|
1136
|
+
result = opts.journal
|
|
1137
|
+
? await opts.journal.effect("ask", `${opts.agentLabel}:${steps}`, produce)
|
|
1138
|
+
: await produce();
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
if (err instanceof AgentLoopHalt) throw err;
|
|
1141
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1142
|
+
throw new AgentLoopHalt("aborted", err, {
|
|
1143
|
+
trail,
|
|
1144
|
+
denials: runDenials,
|
|
1145
|
+
steps,
|
|
1146
|
+
cost,
|
|
1147
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
throw err;
|
|
1151
|
+
}
|
|
626
1152
|
if (result.external !== undefined) {
|
|
627
1153
|
egress.lastExternal = result.external;
|
|
628
1154
|
}
|
|
@@ -630,20 +1156,24 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
630
1156
|
addUsageTokens(tokens, result.usage);
|
|
631
1157
|
lastText = result.text;
|
|
632
1158
|
lastRaw = result.raw !== undefined ? result.raw : result.text;
|
|
1159
|
+
const replayed = opts.journal !== undefined && !executed;
|
|
1160
|
+
const messageId = streamedLive || replayed ? "" : `m-${++messageSeq}`;
|
|
1161
|
+
const emittedText =
|
|
1162
|
+
streamedLive || replayed
|
|
1163
|
+
? false
|
|
1164
|
+
: emitAssistantText(opts.emit ?? (() => undefined), messageId, result.text);
|
|
1165
|
+
if (capHit()) {
|
|
1166
|
+
budgetExceeded = true;
|
|
1167
|
+
stopReason = "budget";
|
|
1168
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
1169
|
+
return finish();
|
|
1170
|
+
}
|
|
633
1171
|
|
|
634
1172
|
const toolCalls = result.toolCalls;
|
|
635
1173
|
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
|
-
};
|
|
1174
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
1175
|
+
stopReason = "completed";
|
|
1176
|
+
return finish();
|
|
647
1177
|
}
|
|
648
1178
|
|
|
649
1179
|
messages.push({
|
|
@@ -652,43 +1182,111 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
652
1182
|
toolCalls,
|
|
653
1183
|
});
|
|
654
1184
|
|
|
655
|
-
for (const tc of toolCalls) {
|
|
1185
|
+
for (const [index, tc] of toolCalls.entries()) {
|
|
656
1186
|
if (steps >= opts.maxSteps) break;
|
|
657
1187
|
steps++;
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1188
|
+
const callId = tc.id.length > 0 ? tc.id : `${opts.agentLabel}:${steps}:${index}`;
|
|
1189
|
+
if (!streamedLive && !replayed) {
|
|
1190
|
+
opts.emit?.({
|
|
1191
|
+
type: "TOOL_CALL_START",
|
|
1192
|
+
toolCallId: callId,
|
|
1193
|
+
toolCallName: tc.name,
|
|
1194
|
+
...(emittedText ? { parentMessageId: messageId } : {}),
|
|
1195
|
+
});
|
|
1196
|
+
opts.emit?.({
|
|
1197
|
+
type: "TOOL_CALL_ARGS",
|
|
1198
|
+
toolCallId: callId,
|
|
1199
|
+
delta: JSON.stringify(tc.arguments ?? {}),
|
|
1200
|
+
});
|
|
1201
|
+
opts.emit?.({ type: "TOOL_CALL_END", toolCallId: callId });
|
|
1202
|
+
}
|
|
1203
|
+
let toolResult: unknown;
|
|
1204
|
+
const spend = { cost: 0, inputTokens: 0, outputTokens: 0 };
|
|
1205
|
+
try {
|
|
1206
|
+
toolResult = await dispatchTool({
|
|
1207
|
+
tool: tc.name,
|
|
1208
|
+
args: tc.arguments,
|
|
1209
|
+
agentLabel: opts.agentLabel,
|
|
1210
|
+
allowedTools: allowed,
|
|
1211
|
+
callTool: opts.callTool,
|
|
1212
|
+
auth: opts.auth,
|
|
1213
|
+
operator: opts.operator,
|
|
1214
|
+
meta: opts.meta,
|
|
1215
|
+
trail,
|
|
1216
|
+
runDenials,
|
|
1217
|
+
callId,
|
|
1218
|
+
step: steps,
|
|
1219
|
+
index,
|
|
1220
|
+
...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
|
|
1221
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
1222
|
+
...(opts.journal !== undefined ? { journal: opts.journal } : {}),
|
|
1223
|
+
...(opts.flow !== undefined ? { flow: opts.flow } : {}),
|
|
1224
|
+
...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
|
|
1225
|
+
...(opts.approvals !== undefined ? { approvals: opts.approvals } : {}),
|
|
1226
|
+
...(opts.emit !== undefined ? { emit: opts.emit } : {}),
|
|
1227
|
+
...(opts.runId !== undefined ? { runId: opts.runId } : {}),
|
|
1228
|
+
...(opts.depth !== undefined ? { depth: opts.depth } : {}),
|
|
1229
|
+
...(opts.maxCostPerRun !== undefined ? { maxCostPerRun: opts.maxCostPerRun } : {}),
|
|
1230
|
+
spent: cost,
|
|
1231
|
+
...(opts.recordCall !== undefined ? { recordCall: opts.recordCall } : {}),
|
|
1232
|
+
spend,
|
|
1233
|
+
});
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
if (err instanceof AgentLoopHalt || isJournalSuspend(err)) throw err;
|
|
1236
|
+
if (err instanceof AiDurableRequiredError) throw err;
|
|
1237
|
+
if (err instanceof Error && err.name === "AgentDenied") {
|
|
1238
|
+
throw new AgentLoopHalt("denied", err, {
|
|
1239
|
+
trail,
|
|
1240
|
+
denials: runDenials,
|
|
1241
|
+
steps,
|
|
1242
|
+
cost,
|
|
1243
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1247
|
+
throw new AgentLoopHalt("aborted", err, {
|
|
1248
|
+
trail,
|
|
1249
|
+
denials: runDenials,
|
|
1250
|
+
steps,
|
|
1251
|
+
cost,
|
|
1252
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
throw new AgentLoopHalt("error", err, {
|
|
1256
|
+
trail,
|
|
1257
|
+
denials: runDenials,
|
|
1258
|
+
steps,
|
|
1259
|
+
cost,
|
|
1260
|
+
output: lastToolResult !== undefined ? lastToolResult : lastRaw,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
671
1263
|
lastToolResult = toolResult;
|
|
1264
|
+
cost += spend.cost;
|
|
1265
|
+
addUsageTokens(tokens, spend);
|
|
1266
|
+
opts.emit?.({
|
|
1267
|
+
type: "TOOL_CALL_RESULT",
|
|
1268
|
+
messageId: `m-${++messageSeq}`,
|
|
1269
|
+
toolCallId: callId,
|
|
1270
|
+
content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult ?? null),
|
|
1271
|
+
role: "tool",
|
|
1272
|
+
});
|
|
672
1273
|
messages.push({
|
|
673
1274
|
role: "tool",
|
|
674
1275
|
content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult ?? null),
|
|
675
|
-
toolCallId:
|
|
1276
|
+
toolCallId: callId,
|
|
676
1277
|
name: tc.name,
|
|
677
1278
|
});
|
|
1279
|
+
if (capHit()) {
|
|
1280
|
+
budgetExceeded = true;
|
|
1281
|
+
stopReason = "budget";
|
|
1282
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
1283
|
+
return finish();
|
|
1284
|
+
}
|
|
678
1285
|
}
|
|
1286
|
+
opts.emit?.({ type: "STEP_FINISHED", stepName });
|
|
679
1287
|
}
|
|
680
1288
|
|
|
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
|
-
};
|
|
1289
|
+
return finish();
|
|
692
1290
|
}
|
|
693
1291
|
|
|
694
1292
|
const runtime: AiRuntime = {
|
|
@@ -721,21 +1319,6 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
721
1319
|
currentAbortSignal(),
|
|
722
1320
|
);
|
|
723
1321
|
|
|
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
1322
|
const via =
|
|
740
1323
|
opts?.via ?? decl.via ?? (decl.model ? [decl.model] : [...models.keys()].slice(0, 1));
|
|
741
1324
|
const attempts: AiFallbackAttempt[] = [];
|
|
@@ -747,7 +1330,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
747
1330
|
const responseFormat = promptResponseFormat(prompt, decl.out);
|
|
748
1331
|
|
|
749
1332
|
const pushJournal = (entry: Omit<AiJournalEntry, "inputTokens" | "outputTokens">): void => {
|
|
750
|
-
journal
|
|
1333
|
+
pushObservability(journal, {
|
|
751
1334
|
...entry,
|
|
752
1335
|
...tokenFields(totalTokens),
|
|
753
1336
|
});
|
|
@@ -777,6 +1360,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
777
1360
|
|
|
778
1361
|
for (const modelName of via) {
|
|
779
1362
|
let sameModelTries = 0;
|
|
1363
|
+
let repaired = false;
|
|
780
1364
|
let advance = true;
|
|
781
1365
|
while (sameModelTries < 2 && advance) {
|
|
782
1366
|
sameModelTries++;
|
|
@@ -785,6 +1369,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
785
1369
|
const client = await clientFor(modelName);
|
|
786
1370
|
let raw: unknown;
|
|
787
1371
|
let attemptCost = 0;
|
|
1372
|
+
const sent: AiMessage[] = [{ role: "user", content: userContent }];
|
|
788
1373
|
|
|
789
1374
|
if (tools.length > 0) {
|
|
790
1375
|
const loop = await toolLoop({
|
|
@@ -812,7 +1397,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
812
1397
|
} else {
|
|
813
1398
|
const result = await client.complete({
|
|
814
1399
|
model: wireModel(modelName, client),
|
|
815
|
-
messages:
|
|
1400
|
+
messages: sent,
|
|
816
1401
|
...(responseFormat !== undefined ? { responseFormat } : {}),
|
|
817
1402
|
...(signal !== undefined ? { signal } : {}),
|
|
818
1403
|
});
|
|
@@ -888,12 +1473,83 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
888
1473
|
at: now(),
|
|
889
1474
|
});
|
|
890
1475
|
}
|
|
1476
|
+
if (decl.repair === 1 && !repaired) {
|
|
1477
|
+
repaired = true;
|
|
1478
|
+
const follow = await client.complete({
|
|
1479
|
+
model: wireModel(modelName, client),
|
|
1480
|
+
messages: [
|
|
1481
|
+
...sent,
|
|
1482
|
+
{
|
|
1483
|
+
role: "user",
|
|
1484
|
+
content: `Schema mismatch: ${err.message}. Reply with JSON only.`,
|
|
1485
|
+
},
|
|
1486
|
+
],
|
|
1487
|
+
...(responseFormat !== undefined ? { responseFormat } : {}),
|
|
1488
|
+
...(signal !== undefined ? { signal } : {}),
|
|
1489
|
+
});
|
|
1490
|
+
const repairCost = follow.usage?.cost ?? 0;
|
|
1491
|
+
totalCost += repairCost;
|
|
1492
|
+
addUsageTokens(totalTokens, follow.usage);
|
|
1493
|
+
attempts.push({
|
|
1494
|
+
model: modelName,
|
|
1495
|
+
ok: true,
|
|
1496
|
+
cost: repairCost,
|
|
1497
|
+
latencyMs: Math.max(0, now() - attemptStart),
|
|
1498
|
+
at: now(),
|
|
1499
|
+
});
|
|
1500
|
+
assertAskBudget(totalCost);
|
|
1501
|
+
let repairRaw: unknown =
|
|
1502
|
+
typeof follow.text === "string" && follow.text.length > 0
|
|
1503
|
+
? follow.text
|
|
1504
|
+
: follow.raw !== undefined
|
|
1505
|
+
? follow.raw
|
|
1506
|
+
: follow.text;
|
|
1507
|
+
try {
|
|
1508
|
+
const coerced = coerceModelObject(repairRaw);
|
|
1509
|
+
const prepared = outExpectsVia(decl.out)
|
|
1510
|
+
? { ...coerced, via: modelName }
|
|
1511
|
+
: coerced;
|
|
1512
|
+
const validated = validatePromptOut(prompt, version, decl.out, prepared);
|
|
1513
|
+
const output = { ...validated, via: modelName };
|
|
1514
|
+
if (journalingForced) {
|
|
1515
|
+
pushJournal({
|
|
1516
|
+
prompt,
|
|
1517
|
+
...(version !== undefined ? { version } : {}),
|
|
1518
|
+
input,
|
|
1519
|
+
output,
|
|
1520
|
+
attempts: [...attempts],
|
|
1521
|
+
outcome: "ok",
|
|
1522
|
+
cost: totalCost,
|
|
1523
|
+
latencyMs: Math.max(0, now() - started),
|
|
1524
|
+
at: now(),
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
return output;
|
|
1528
|
+
} catch (again) {
|
|
1529
|
+
if (again instanceof AiSchemaValidationError && journalingForced) {
|
|
1530
|
+
pushJournal({
|
|
1531
|
+
prompt,
|
|
1532
|
+
...(version !== undefined ? { version } : {}),
|
|
1533
|
+
input,
|
|
1534
|
+
output: coerceModelObject(repairRaw),
|
|
1535
|
+
attempts: [...attempts],
|
|
1536
|
+
outcome: "schema_invalid",
|
|
1537
|
+
cost: totalCost,
|
|
1538
|
+
latencyMs: Math.max(0, now() - started),
|
|
1539
|
+
schemaMismatch: again.mismatch,
|
|
1540
|
+
at: now(),
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
throw again;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
891
1546
|
throw err;
|
|
892
1547
|
}
|
|
893
1548
|
throw err;
|
|
894
1549
|
}
|
|
895
1550
|
} catch (err) {
|
|
896
1551
|
if (err instanceof AiSchemaValidationError) throw err;
|
|
1552
|
+
if (err instanceof Error && err.name === "AiBudgetExceededError") throw err;
|
|
897
1553
|
lastError = err instanceof Error ? err.message : String(err);
|
|
898
1554
|
attempts.push({
|
|
899
1555
|
model: modelName,
|
|
@@ -955,49 +1611,456 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
955
1611
|
const modelName = decl.model ?? [...models.keys()][0] ?? "mock";
|
|
956
1612
|
const client = await clientFor(modelName);
|
|
957
1613
|
const started = now();
|
|
1614
|
+
const runId = await allocateAgentRunId(agent, runOpts);
|
|
1615
|
+
runOpts.onRunId?.(runId);
|
|
1616
|
+
const threadId = runOpts.threadId ?? loadOkid().okid();
|
|
1617
|
+
const logHeader = agentLogHeader(runId, threadId, agent, runOpts);
|
|
1618
|
+
const safeAppend = async (event: AgUiEvent): Promise<void> => {
|
|
1619
|
+
try {
|
|
1620
|
+
await eventLog.append(runId, event, now());
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
if (runOpts.journal) {
|
|
1626
|
+
await eventLog.open(logHeader);
|
|
1627
|
+
await safeAppend({ type: "RUN_STARTED", threadId, runId });
|
|
1628
|
+
}
|
|
958
1629
|
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1630
|
+
const remember = (partial: {
|
|
1631
|
+
readonly ok: boolean;
|
|
1632
|
+
readonly stopReason: AgentStopReason;
|
|
1633
|
+
readonly steps: number;
|
|
1634
|
+
readonly trail: readonly AgentToolStep[];
|
|
1635
|
+
readonly denials: readonly AgentDenial[];
|
|
1636
|
+
readonly output: unknown;
|
|
1637
|
+
readonly cost: number;
|
|
1638
|
+
readonly error?: string;
|
|
1639
|
+
}) => {
|
|
1640
|
+
const record: AgentRunRecord = {
|
|
1641
|
+
id: runId,
|
|
1642
|
+
agent,
|
|
1643
|
+
message: agentMessageLabel(runOpts),
|
|
1644
|
+
...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
|
|
1645
|
+
ok: partial.ok,
|
|
1646
|
+
stopReason: partial.stopReason,
|
|
1647
|
+
...(partial.error !== undefined ? { error: partial.error } : {}),
|
|
1648
|
+
steps: partial.steps,
|
|
1649
|
+
trail: partial.trail,
|
|
1650
|
+
denials: partial.denials,
|
|
1651
|
+
output: partial.output,
|
|
1652
|
+
at: started,
|
|
1653
|
+
finishedAt: now(),
|
|
1654
|
+
threadId,
|
|
1655
|
+
cost: partial.cost,
|
|
1656
|
+
...(loopTokens.inputTokens !== undefined ? { inputTokens: loopTokens.inputTokens } : {}),
|
|
1657
|
+
...(loopTokens.outputTokens !== undefined
|
|
1658
|
+
? { outputTokens: loopTokens.outputTokens }
|
|
1659
|
+
: {}),
|
|
1660
|
+
};
|
|
1661
|
+
pushObservability(agentRuns, record);
|
|
1662
|
+
return {
|
|
1663
|
+
runId,
|
|
1664
|
+
ok: record.ok,
|
|
1665
|
+
stopReason: record.stopReason,
|
|
1666
|
+
steps: record.steps,
|
|
1667
|
+
denials: record.denials,
|
|
1668
|
+
trail: record.trail,
|
|
1669
|
+
output: record.output,
|
|
1670
|
+
cost: record.cost,
|
|
1671
|
+
...(loopTokens.inputTokens !== undefined ? { inputTokens: loopTokens.inputTokens } : {}),
|
|
1672
|
+
...(loopTokens.outputTokens !== undefined
|
|
1673
|
+
? { outputTokens: loopTokens.outputTokens }
|
|
1674
|
+
: {}),
|
|
1675
|
+
};
|
|
1676
|
+
};
|
|
1677
|
+
let loopTokens: { inputTokens?: number; outputTokens?: number } = {};
|
|
1678
|
+
let appendChain: Promise<unknown> = Promise.resolve();
|
|
1679
|
+
|
|
1680
|
+
try {
|
|
1681
|
+
const loop = await toolLoop({
|
|
1682
|
+
client,
|
|
1683
|
+
modelName,
|
|
1684
|
+
messages: agentMessages(runOpts),
|
|
1685
|
+
tools: decl.tools,
|
|
1686
|
+
maxSteps,
|
|
1687
|
+
...(runOpts.maxCostPerRun !== undefined
|
|
1688
|
+
? { maxCostPerRun: runOpts.maxCostPerRun }
|
|
1689
|
+
: decl.budget?.maxCostPerRun !== undefined
|
|
1690
|
+
? { maxCostPerRun: decl.budget.maxCostPerRun }
|
|
1691
|
+
: {}),
|
|
1692
|
+
agentLabel: agent,
|
|
1693
|
+
runId,
|
|
1694
|
+
depth: runOpts.depth ?? 1,
|
|
1695
|
+
...(runOpts.recordCall !== undefined ? { recordCall: runOpts.recordCall } : {}),
|
|
1696
|
+
callTool: runOpts.callTool,
|
|
1697
|
+
auth: runOpts.auth,
|
|
1698
|
+
operator: runOpts.operator,
|
|
1699
|
+
meta: runOpts.meta,
|
|
1700
|
+
signal: currentAbortSignal(),
|
|
1701
|
+
threadId,
|
|
1702
|
+
...(runOpts.journal !== undefined
|
|
1703
|
+
? {
|
|
1704
|
+
emit: (event: AgUiEvent) => {
|
|
1705
|
+
appendChain = appendChain.then(() => safeAppend(event));
|
|
1706
|
+
},
|
|
1707
|
+
}
|
|
1708
|
+
: {}),
|
|
1709
|
+
...(runOpts.journal !== undefined ? { journal: runOpts.journal } : {}),
|
|
1710
|
+
...(runOpts.flow !== undefined ? { flow: runOpts.flow } : {}),
|
|
1711
|
+
...(runOpts.tenantId !== undefined ? { tenantId: runOpts.tenantId } : {}),
|
|
1712
|
+
...(decl.approvals !== undefined ? { approvals: decl.approvals } : {}),
|
|
1713
|
+
});
|
|
1714
|
+
await appendChain;
|
|
1715
|
+
loopTokens = tokenFields(loop);
|
|
1716
|
+
const settled = remember({
|
|
1717
|
+
ok: loop.stopReason === "completed" && loop.denials.length === 0,
|
|
1718
|
+
stopReason: loop.stopReason,
|
|
1719
|
+
steps: loop.steps,
|
|
1720
|
+
trail: loop.trail,
|
|
1721
|
+
denials: loop.denials,
|
|
1722
|
+
output: loop.output,
|
|
1723
|
+
cost: loop.cost,
|
|
1724
|
+
});
|
|
1725
|
+
if (runOpts.journal) {
|
|
1726
|
+
await safeAppend({
|
|
1727
|
+
type: "RUN_FINISHED",
|
|
1728
|
+
threadId,
|
|
1729
|
+
runId,
|
|
1730
|
+
result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
|
|
1731
|
+
...(loopTokens.inputTokens !== undefined || loopTokens.outputTokens !== undefined
|
|
1732
|
+
? { usage: [tokenFields(loopTokens)] }
|
|
1733
|
+
: {}),
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
return settled;
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
if (err instanceof AgentLoopHalt) {
|
|
1739
|
+
const result = remember({
|
|
1740
|
+
ok: false,
|
|
1741
|
+
stopReason: err.stopReason,
|
|
1742
|
+
steps: err.steps,
|
|
1743
|
+
trail: err.trail,
|
|
1744
|
+
denials: err.denials,
|
|
1745
|
+
output: err.output,
|
|
1746
|
+
cost: err.cost,
|
|
1747
|
+
...(err.stopReason === "error"
|
|
1748
|
+
? { error: err.cause instanceof Error ? err.cause.message : String(err.cause) }
|
|
1749
|
+
: {}),
|
|
1750
|
+
});
|
|
1751
|
+
if (err.stopReason === "aborted" || err.stopReason === "error") {
|
|
1752
|
+
if (runOpts.journal) {
|
|
1753
|
+
const cause = err.cause instanceof Error ? err.cause : undefined;
|
|
1754
|
+
await safeAppend({
|
|
1755
|
+
type: "RUN_ERROR",
|
|
1756
|
+
message: cause?.message ?? String(err.cause),
|
|
1757
|
+
...(cause && cause.name !== "Error" ? { code: cause.name } : {}),
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
throw err.cause;
|
|
1761
|
+
}
|
|
1762
|
+
if (runOpts.journal) {
|
|
1763
|
+
await safeAppend({
|
|
1764
|
+
type: "RUN_FINISHED",
|
|
1765
|
+
threadId,
|
|
1766
|
+
runId,
|
|
1767
|
+
result: {
|
|
1768
|
+
cost: err.cost,
|
|
1769
|
+
stopReason: err.stopReason,
|
|
1770
|
+
output: err.output,
|
|
1771
|
+
},
|
|
1772
|
+
...(loopTokens.inputTokens !== undefined || loopTokens.outputTokens !== undefined
|
|
1773
|
+
? { usage: [tokenFields(loopTokens)] }
|
|
1774
|
+
: {}),
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
return result;
|
|
1778
|
+
}
|
|
1779
|
+
if (runOpts.journal) {
|
|
1780
|
+
const error = err instanceof Error ? err : undefined;
|
|
1781
|
+
await safeAppend({
|
|
1782
|
+
type: "RUN_ERROR",
|
|
1783
|
+
message: error?.message ?? String(err),
|
|
1784
|
+
...(error && error.name !== "Error" ? { code: error.name } : {}),
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
976
1787
|
throw err;
|
|
977
1788
|
}
|
|
1789
|
+
},
|
|
978
1790
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1791
|
+
streamAsk(prompt, input, opts) {
|
|
1792
|
+
const queue = createEventQueue();
|
|
1793
|
+
const runId = `ask-${++runSeq}`;
|
|
1794
|
+
const signal = currentAbortSignal();
|
|
1795
|
+
void (async () => {
|
|
1796
|
+
try {
|
|
1797
|
+
queue.emit({ type: "RUN_STARTED", threadId: "default", runId });
|
|
1798
|
+
const tools = opts?.tools ?? [];
|
|
1799
|
+
if (tools.length > 0) {
|
|
1800
|
+
const pin = parsePromptRef(prompt);
|
|
1801
|
+
const decl = prompts.get(pin.name) ?? prompts.get(prompt);
|
|
1802
|
+
if (!decl) throw new Error(`ai: unknown prompt "${prompt}"`);
|
|
1803
|
+
const via =
|
|
1804
|
+
opts?.via ?? decl.via ?? (decl.model ? [decl.model] : [...models.keys()].slice(0, 1));
|
|
1805
|
+
const modelName = via[0];
|
|
1806
|
+
if (!modelName) throw new Error(`ai: no model for prompt "${prompt}"`);
|
|
1807
|
+
const client = await clientFor(modelName);
|
|
1808
|
+
const loop = await toolLoop({
|
|
1809
|
+
client,
|
|
1810
|
+
modelName,
|
|
1811
|
+
messages: [{ role: "user", content: askUserContent(input, decl.out) }],
|
|
1812
|
+
tools,
|
|
1813
|
+
maxSteps: opts?.maxSteps ?? AI_DEFAULT_MAX_STEPS,
|
|
1814
|
+
agentLabel: prompt,
|
|
1815
|
+
callTool: opts?.callTool,
|
|
1816
|
+
emit: queue.emit,
|
|
1817
|
+
signal,
|
|
1818
|
+
});
|
|
1819
|
+
queue.emit({
|
|
1820
|
+
type: "RUN_FINISHED",
|
|
1821
|
+
threadId: "default",
|
|
1822
|
+
runId,
|
|
1823
|
+
result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
|
|
1824
|
+
...(tokenFields(loop).inputTokens !== undefined ||
|
|
1825
|
+
tokenFields(loop).outputTokens !== undefined
|
|
1826
|
+
? { usage: [tokenFields(loop)] }
|
|
1827
|
+
: {}),
|
|
1828
|
+
});
|
|
1829
|
+
} else {
|
|
1830
|
+
const output = await runtime.ask(prompt, input, opts);
|
|
1831
|
+
emitAssistantText(queue.emit, "m-1", JSON.stringify(output));
|
|
1832
|
+
const spent = journal.at(-1)?.cost ?? 0;
|
|
1833
|
+
queue.emit({
|
|
1834
|
+
type: "RUN_FINISHED",
|
|
1835
|
+
threadId: "default",
|
|
1836
|
+
runId,
|
|
1837
|
+
result: { cost: spent, stopReason: "completed", output },
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
queue.finish();
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
queue.emit({
|
|
1843
|
+
type: "RUN_ERROR",
|
|
1844
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1845
|
+
...(err instanceof Error && err.name !== "Error" ? { code: err.name } : {}),
|
|
1846
|
+
});
|
|
1847
|
+
queue.finish();
|
|
1848
|
+
}
|
|
1849
|
+
})();
|
|
1850
|
+
return queue.events;
|
|
1851
|
+
},
|
|
1852
|
+
|
|
1853
|
+
streamAgent(agent, runOpts) {
|
|
1854
|
+
const queue = createEventQueue();
|
|
1855
|
+
const signal = currentAbortSignal();
|
|
1856
|
+
const threadId = runOpts.threadId ?? loadOkid().okid();
|
|
1857
|
+
let skipLoggedStart = false;
|
|
1858
|
+
let runId = runOpts.runId ?? "";
|
|
1859
|
+
let appendChain: Promise<unknown> = Promise.resolve();
|
|
1860
|
+
const emit: AgentEventEmit = (event) => {
|
|
1861
|
+
if (!runOpts.journal) {
|
|
1862
|
+
queue.emit(event);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (skipLoggedStart && event.type === "RUN_STARTED") {
|
|
1866
|
+
queue.emit(event);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
appendChain = appendChain.then(async () => {
|
|
1870
|
+
let seq: number | undefined;
|
|
1871
|
+
try {
|
|
1872
|
+
seq = await eventLog.append(runId, event, now());
|
|
1873
|
+
} catch (err) {
|
|
1874
|
+
noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
|
|
1875
|
+
}
|
|
1876
|
+
queue.emit(seq !== undefined ? loadSseId().withSseId(event, String(seq)) : event);
|
|
1877
|
+
});
|
|
1000
1878
|
};
|
|
1879
|
+
let resolveResult: (value: unknown) => void = () => undefined;
|
|
1880
|
+
let rejectResult: (err: unknown) => void = () => undefined;
|
|
1881
|
+
const result = new Promise<unknown>((resolve, reject) => {
|
|
1882
|
+
resolveResult = resolve;
|
|
1883
|
+
rejectResult = reject;
|
|
1884
|
+
});
|
|
1885
|
+
// HTTP drains the iterator and may never await `result`.
|
|
1886
|
+
void result.catch(() => undefined);
|
|
1887
|
+
void (async () => {
|
|
1888
|
+
let closed = false;
|
|
1889
|
+
const close = (err?: unknown): void => {
|
|
1890
|
+
if (closed) return;
|
|
1891
|
+
closed = true;
|
|
1892
|
+
queue.finish(err);
|
|
1893
|
+
};
|
|
1894
|
+
try {
|
|
1895
|
+
if (runOpts.journal) {
|
|
1896
|
+
runId = await allocateAgentRunId(agent, runOpts);
|
|
1897
|
+
runOpts.onRunId?.(runId);
|
|
1898
|
+
await eventLog.open(agentLogHeader(runId, threadId, agent, runOpts));
|
|
1899
|
+
const prior = await eventLog.read(runId, 0);
|
|
1900
|
+
skipLoggedStart = prior.some((row) => row.event.type === "RUN_STARTED");
|
|
1901
|
+
} else {
|
|
1902
|
+
runId = runOpts.runId ?? loadOkid().okid();
|
|
1903
|
+
runOpts.onRunId?.(runId);
|
|
1904
|
+
}
|
|
1905
|
+
emit({ type: "RUN_STARTED", threadId, runId });
|
|
1906
|
+
const decl = agents.get(agent);
|
|
1907
|
+
if (!decl) throw new Error(`ai: unknown agent "${agent}"`);
|
|
1908
|
+
const maxSteps = decl.maxSteps ?? AI_DEFAULT_MAX_STEPS;
|
|
1909
|
+
const modelName = decl.model ?? [...models.keys()][0] ?? "mock";
|
|
1910
|
+
const client = await clientFor(modelName);
|
|
1911
|
+
const started = now();
|
|
1912
|
+
const loop = await toolLoop({
|
|
1913
|
+
client,
|
|
1914
|
+
modelName,
|
|
1915
|
+
messages: agentMessages(runOpts),
|
|
1916
|
+
tools: decl.tools,
|
|
1917
|
+
maxSteps,
|
|
1918
|
+
...(runOpts.maxCostPerRun !== undefined
|
|
1919
|
+
? { maxCostPerRun: runOpts.maxCostPerRun }
|
|
1920
|
+
: decl.budget?.maxCostPerRun !== undefined
|
|
1921
|
+
? { maxCostPerRun: decl.budget.maxCostPerRun }
|
|
1922
|
+
: {}),
|
|
1923
|
+
agentLabel: agent,
|
|
1924
|
+
runId,
|
|
1925
|
+
depth: runOpts.depth ?? 1,
|
|
1926
|
+
...(runOpts.recordCall !== undefined ? { recordCall: runOpts.recordCall } : {}),
|
|
1927
|
+
callTool: runOpts.callTool,
|
|
1928
|
+
auth: runOpts.auth,
|
|
1929
|
+
operator: runOpts.operator,
|
|
1930
|
+
meta: runOpts.meta,
|
|
1931
|
+
emit,
|
|
1932
|
+
signal,
|
|
1933
|
+
threadId,
|
|
1934
|
+
...(runOpts.journal !== undefined ? { journal: runOpts.journal } : {}),
|
|
1935
|
+
...(runOpts.flow !== undefined ? { flow: runOpts.flow } : {}),
|
|
1936
|
+
...(runOpts.tenantId !== undefined ? { tenantId: runOpts.tenantId } : {}),
|
|
1937
|
+
...(decl.approvals !== undefined ? { approvals: decl.approvals } : {}),
|
|
1938
|
+
});
|
|
1939
|
+
const record: AgentRunRecord = {
|
|
1940
|
+
id: runId,
|
|
1941
|
+
agent,
|
|
1942
|
+
message: agentMessageLabel(runOpts),
|
|
1943
|
+
...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
|
|
1944
|
+
ok: loop.stopReason === "completed" && loop.denials.length === 0,
|
|
1945
|
+
stopReason: loop.stopReason,
|
|
1946
|
+
steps: loop.steps,
|
|
1947
|
+
trail: loop.trail,
|
|
1948
|
+
denials: loop.denials,
|
|
1949
|
+
output: loop.output,
|
|
1950
|
+
at: started,
|
|
1951
|
+
cost: loop.cost,
|
|
1952
|
+
};
|
|
1953
|
+
pushObservability(agentRuns, record);
|
|
1954
|
+
const usage = tokenFields(loop);
|
|
1955
|
+
const settled = {
|
|
1956
|
+
ok: record.ok,
|
|
1957
|
+
stopReason: record.stopReason,
|
|
1958
|
+
steps: record.steps,
|
|
1959
|
+
denials: record.denials,
|
|
1960
|
+
trail: record.trail,
|
|
1961
|
+
output: record.output,
|
|
1962
|
+
cost: record.cost,
|
|
1963
|
+
...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
|
|
1964
|
+
...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
|
|
1965
|
+
};
|
|
1966
|
+
resolveResult(settled);
|
|
1967
|
+
await appendChain;
|
|
1968
|
+
emit({
|
|
1969
|
+
type: "RUN_FINISHED",
|
|
1970
|
+
threadId,
|
|
1971
|
+
runId,
|
|
1972
|
+
result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
|
|
1973
|
+
...(usage.inputTokens !== undefined || usage.outputTokens !== undefined
|
|
1974
|
+
? { usage: [usage] }
|
|
1975
|
+
: {}),
|
|
1976
|
+
});
|
|
1977
|
+
await appendChain;
|
|
1978
|
+
close();
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
if (isJournalSuspend(err)) {
|
|
1981
|
+
await appendChain;
|
|
1982
|
+
rejectResult(err);
|
|
1983
|
+
close(err);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (err instanceof AiDurableRequiredError) {
|
|
1987
|
+
emit({
|
|
1988
|
+
type: "RUN_ERROR",
|
|
1989
|
+
message: err.message,
|
|
1990
|
+
code: err.name,
|
|
1991
|
+
});
|
|
1992
|
+
await appendChain;
|
|
1993
|
+
rejectResult(err);
|
|
1994
|
+
close();
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (err instanceof AgentLoopHalt) {
|
|
1998
|
+
const message = err.cause instanceof Error ? err.cause.message : String(err.cause);
|
|
1999
|
+
pushObservability(agentRuns, {
|
|
2000
|
+
id: runId,
|
|
2001
|
+
agent,
|
|
2002
|
+
message: agentMessageLabel(runOpts),
|
|
2003
|
+
ok: false,
|
|
2004
|
+
stopReason: err.stopReason,
|
|
2005
|
+
...(err.stopReason === "error" ? { error: message } : {}),
|
|
2006
|
+
steps: err.steps,
|
|
2007
|
+
trail: err.trail,
|
|
2008
|
+
denials: err.denials,
|
|
2009
|
+
output: err.output,
|
|
2010
|
+
at: now(),
|
|
2011
|
+
cost: err.cost,
|
|
2012
|
+
});
|
|
2013
|
+
if (err.stopReason === "error") {
|
|
2014
|
+
const settled = {
|
|
2015
|
+
ok: false,
|
|
2016
|
+
stopReason: err.stopReason,
|
|
2017
|
+
error: message,
|
|
2018
|
+
steps: err.steps,
|
|
2019
|
+
denials: err.denials,
|
|
2020
|
+
trail: err.trail,
|
|
2021
|
+
output: err.output,
|
|
2022
|
+
cost: err.cost,
|
|
2023
|
+
};
|
|
2024
|
+
resolveResult(settled);
|
|
2025
|
+
await appendChain;
|
|
2026
|
+
emit({
|
|
2027
|
+
type: "RUN_FINISHED",
|
|
2028
|
+
threadId,
|
|
2029
|
+
runId,
|
|
2030
|
+
result: {
|
|
2031
|
+
cost: err.cost,
|
|
2032
|
+
stopReason: "error",
|
|
2033
|
+
output: err.output,
|
|
2034
|
+
error: message,
|
|
2035
|
+
},
|
|
2036
|
+
});
|
|
2037
|
+
await appendChain;
|
|
2038
|
+
close();
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2043
|
+
rejectResult(err);
|
|
2044
|
+
emit({
|
|
2045
|
+
type: "RUN_ERROR",
|
|
2046
|
+
message,
|
|
2047
|
+
...(err instanceof Error && err.name !== "Error" ? { code: err.name } : {}),
|
|
2048
|
+
});
|
|
2049
|
+
await appendChain;
|
|
2050
|
+
close();
|
|
2051
|
+
} finally {
|
|
2052
|
+
try {
|
|
2053
|
+
await appendChain;
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
|
|
2056
|
+
}
|
|
2057
|
+
if (!closed) {
|
|
2058
|
+
queue.emit({ type: "RUN_ERROR", message: "agent run ended" });
|
|
2059
|
+
close();
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
})();
|
|
2063
|
+
return Object.assign(queue.events, { result });
|
|
1001
2064
|
},
|
|
1002
2065
|
|
|
1003
2066
|
async *stream(model, streamOpts) {
|
|
@@ -1062,6 +2125,23 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
1062
2125
|
await index.upsert(id, vector, { text });
|
|
1063
2126
|
},
|
|
1064
2127
|
|
|
2128
|
+
async resolveApproval(id, decision, ctx) {
|
|
2129
|
+
const store = options.journalStore;
|
|
2130
|
+
if (!store) return { ok: false, status: 404 };
|
|
2131
|
+
const pending = await readAgentApproval(store, id);
|
|
2132
|
+
if (!pending) return { ok: false, status: 404 };
|
|
2133
|
+
if ((decision.tenant ?? null) !== (pending.tenant ?? null)) {
|
|
2134
|
+
return { ok: false, status: 404 };
|
|
2135
|
+
}
|
|
2136
|
+
if (options.gates) {
|
|
2137
|
+
const allowed = await options.gates.allow([pending.gate], ctx);
|
|
2138
|
+
if (!allowed) return { ok: false, status: 403 };
|
|
2139
|
+
} else if (pending.gate !== "public") {
|
|
2140
|
+
return { ok: false, status: 403 };
|
|
2141
|
+
}
|
|
2142
|
+
return resolveAgentApproval(store, id, decision, now);
|
|
2143
|
+
},
|
|
2144
|
+
|
|
1065
2145
|
async embedVector(modelName, text) {
|
|
1066
2146
|
const client = await clientFor(modelName);
|
|
1067
2147
|
if (!client.embed) {
|
|
@@ -1076,6 +2156,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
1076
2156
|
return vector;
|
|
1077
2157
|
},
|
|
1078
2158
|
};
|
|
2159
|
+
self = runtime;
|
|
1079
2160
|
return runtime;
|
|
1080
2161
|
}
|
|
1081
2162
|
|