@opengeni/sdk 3.3.2 → 3.4.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/README.md +1007 -72
- package/dist/adapters/express.d.ts +31 -0
- package/dist/adapters/express.js +87 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/hono.d.ts +16 -0
- package/dist/adapters/hono.js +8 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/next.d.ts +29 -0
- package/dist/adapters/next.js +22 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/allowance-refusal.d.ts +2 -0
- package/dist/allowance-refusal.js +50 -0
- package/dist/allowance-refusal.js.map +1 -0
- package/dist/app-links.d.ts +42 -0
- package/dist/app-links.js +66 -0
- package/dist/app-links.js.map +1 -0
- package/dist/artifact-catalog.d.ts +40 -0
- package/dist/artifact-client.d.ts +20 -8
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +8 -6
- package/dist/automations.js +8 -2
- package/dist/automations.js.map +1 -1
- package/dist/azure-live-transport.d.ts +32 -0
- package/dist/browser.d.ts +7 -2
- package/dist/browser.js +27 -5
- package/dist/chat/fold.d.ts +75 -0
- package/dist/chat/handler.d.ts +41 -0
- package/dist/chat/http.d.ts +67 -0
- package/dist/chat/ids.d.ts +34 -0
- package/dist/chat/index.d.ts +9 -0
- package/dist/chat/index.js +1369 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/openai.d.ts +30 -0
- package/dist/chat/opengeni.d.ts +129 -0
- package/dist/chat/types.d.ts +207 -0
- package/dist/chat/vercel.d.ts +49 -0
- package/dist/chats.d.ts +8 -0
- package/dist/chunk-4AHR4KGA.js +301 -0
- package/dist/chunk-4AHR4KGA.js.map +1 -0
- package/dist/chunk-6GYGMLX4.js +332 -0
- package/dist/chunk-6GYGMLX4.js.map +1 -0
- package/dist/chunk-7EWT3PV6.js +368 -0
- package/dist/chunk-7EWT3PV6.js.map +1 -0
- package/dist/chunk-FTWVVPR6.js +1665 -0
- package/dist/chunk-FTWVVPR6.js.map +1 -0
- package/dist/{chunk-VYCTL62C.js → chunk-GZQSWAKD.js} +1 -1
- package/dist/chunk-GZQSWAKD.js.map +1 -0
- package/dist/chunk-KGEABAID.js +71 -0
- package/dist/chunk-KGEABAID.js.map +1 -0
- package/dist/{chunk-YKZME56C.js → chunk-KWADQ227.js} +3 -3
- package/dist/{chunk-YKZME56C.js.map → chunk-KWADQ227.js.map} +1 -1
- package/dist/{chunk-JUS2UYRP.js → chunk-NCU5OAN7.js} +27 -2
- package/dist/chunk-NCU5OAN7.js.map +1 -0
- package/dist/{chunk-YXQX7RQT.js → chunk-OI5YLASH.js} +73 -2
- package/dist/chunk-OI5YLASH.js.map +1 -0
- package/dist/chunk-OV3POE62.js +359 -0
- package/dist/chunk-OV3POE62.js.map +1 -0
- package/dist/chunk-P7UWKORG.js +561 -0
- package/dist/chunk-P7UWKORG.js.map +1 -0
- package/dist/chunk-QGEIL4TR.js +195 -0
- package/dist/chunk-QGEIL4TR.js.map +1 -0
- package/dist/{chunk-TRNI7KEO.js → chunk-S2NLS3ES.js} +3669 -1281
- package/dist/chunk-S2NLS3ES.js.map +1 -0
- package/dist/chunk-UM4ZXXJR.js +9 -0
- package/dist/chunk-UM4ZXXJR.js.map +1 -0
- package/dist/{chunk-QT3KFVDP.js → chunk-UME3X7CP.js} +28 -32
- package/dist/chunk-UME3X7CP.js.map +1 -0
- package/dist/chunk-VT4YM4XQ.js +210 -0
- package/dist/chunk-VT4YM4XQ.js.map +1 -0
- package/dist/chunk-WSYYXONG.js +1602 -0
- package/dist/chunk-WSYYXONG.js.map +1 -0
- package/dist/{chunk-C2H7HBNP.js → chunk-WVCJRSGJ.js} +119 -14
- package/dist/chunk-WVCJRSGJ.js.map +1 -0
- package/dist/chunk-X5ITQVXE.js +15 -0
- package/dist/chunk-X5ITQVXE.js.map +1 -0
- package/dist/chunk-XA7HVIHI.js +126 -0
- package/dist/chunk-XA7HVIHI.js.map +1 -0
- package/dist/{chunk-KKRO2VCP.js → chunk-YJCHB7R3.js} +60 -12
- package/dist/chunk-YJCHB7R3.js.map +1 -0
- package/dist/chunk-YKIHNLWD.js +153 -0
- package/dist/chunk-YKIHNLWD.js.map +1 -0
- package/dist/chunk-ZDUBPV33.js +277 -0
- package/dist/chunk-ZDUBPV33.js.map +1 -0
- package/dist/client.d.ts +1068 -118
- package/dist/codex-realtime-controller.d.ts +13 -0
- package/dist/codex-realtime-controller.js +4 -2
- package/dist/codex-realtime-v3-wire.d.ts +3 -2
- package/dist/codex-realtime.d.ts +5 -1
- package/dist/company-brain.d.ts +1 -11
- package/dist/core.d.ts +7 -2
- package/dist/core.js +25 -6
- package/dist/deprecation.d.ts +47 -0
- package/dist/document-authority-client.d.ts +24 -2
- package/dist/document-authority.js +7 -5
- package/dist/editable-artifacts/index.d.ts +1 -1
- package/dist/editable-artifacts/types.d.ts +12 -2
- package/dist/editable-artifacts.js +19 -3
- package/dist/editable-artifacts.js.map +1 -1
- package/dist/embedding-client.d.ts +129 -0
- package/dist/errors.d.ts +76 -1
- package/dist/external-vault-receiver.d.ts +203 -0
- package/dist/external-vault-receiver.js +564 -0
- package/dist/external-vault-receiver.js.map +1 -0
- package/dist/feedback.d.ts +23 -0
- package/dist/gateway-realtime-transport.js +1 -1
- package/dist/github-repositories.d.ts +10 -0
- package/dist/github-repositories.js +36 -0
- package/dist/github-repositories.js.map +1 -0
- package/dist/index.d.ts +62 -11
- package/dist/index.js +557 -338
- package/dist/index.js.map +1 -1
- package/dist/insights-usage.d.ts +33 -0
- package/dist/insights-usage.js +7 -0
- package/dist/insights-usage.js.map +1 -0
- package/dist/interaction.d.ts +165 -71
- package/dist/interaction.js +2 -2
- package/dist/knowledge.d.ts +7 -0
- package/dist/knowledge.js +20 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/known-tools.d.ts +4 -0
- package/dist/message-links.d.ts +68 -0
- package/dist/model-context.d.ts +68 -0
- package/dist/model-display.d.ts +2 -0
- package/dist/model-display.js +17 -0
- package/dist/model-display.js.map +1 -0
- package/dist/model-picker-order.d.ts +1 -0
- package/dist/model-picker-order.js +10 -0
- package/dist/model-picker-order.js.map +1 -0
- package/dist/organization-integration-policy.d.ts +26 -0
- package/dist/organization-integration-policy.js +26 -0
- package/dist/organization-integration-policy.js.map +1 -0
- package/dist/pr-review-client.d.ts +1 -1
- package/dist/pr-review.d.ts +1 -1
- package/dist/pr-review.js.map +1 -1
- package/dist/preference-registry.d.ts +1 -1
- package/dist/prepared-mcp-setup.d.ts +5 -0
- package/dist/proxy.d.ts +13 -11
- package/dist/realtime.d.ts +1 -1
- package/dist/realtime.js +253 -7
- package/dist/realtime.js.map +1 -1
- package/dist/session-history-import.d.ts +22 -0
- package/dist/session-history-import.js +36 -0
- package/dist/session-history-import.js.map +1 -0
- package/dist/session-list-entries.d.ts +1 -0
- package/dist/session-list-entries.js +7 -0
- package/dist/session-list-entries.js.map +1 -0
- package/dist/session-message-search.d.ts +53 -0
- package/dist/session-proxy-client.d.ts +28 -0
- package/dist/session-proxy.d.ts +334 -0
- package/dist/session-proxy.js +24 -0
- package/dist/session-proxy.js.map +1 -0
- package/dist/session-titles.d.ts +2 -28
- package/dist/session-usage-limits.d.ts +10 -0
- package/dist/session-usage-limits.js +43 -0
- package/dist/session-usage-limits.js.map +1 -0
- package/dist/shared-catalog-loader.d.ts +34 -0
- package/dist/site-ai.d.ts +35 -0
- package/dist/site-bridge-host.d.ts +112 -0
- package/dist/site-bridge-host.js +286 -0
- package/dist/site-bridge-host.js.map +1 -0
- package/dist/site-browser-entry.d.ts +1 -0
- package/dist/site-browser-runtime.gen.d.ts +1 -0
- package/dist/site-data.d.ts +178 -0
- package/dist/site-document.d.ts +5 -0
- package/dist/site-document.js +20 -0
- package/dist/site-document.js.map +1 -0
- package/dist/site-document.test.d.ts +1 -0
- package/dist/site-http.d.ts +17 -0
- package/dist/site-sql.d.ts +31 -0
- package/dist/site-tool-bridge.d.ts +57 -0
- package/dist/site.d.ts +186 -0
- package/dist/site.js +63 -0
- package/dist/site.js.map +1 -0
- package/dist/skills.d.ts +75 -0
- package/dist/sse.d.ts +1 -1
- package/dist/stream.d.ts +2 -0
- package/dist/tenant-workspaces.d.ts +47 -0
- package/dist/tenant-workspaces.js +8 -0
- package/dist/tenant-workspaces.js.map +1 -0
- package/dist/tool-auth.d.ts +87 -0
- package/dist/tool-auth.js +23 -0
- package/dist/tool-auth.js.map +1 -0
- package/dist/tool-display-metadata.d.ts +3 -0
- package/dist/tools-37DCTCNZ.js +12 -0
- package/dist/tools-37DCTCNZ.js.map +1 -0
- package/dist/tools.d.ts +81 -0
- package/dist/transcription.d.ts +1 -1
- package/dist/types.d.ts +2807 -583
- package/dist/usage-allowances.d.ts +36 -0
- package/dist/usage-allowances.js +80 -0
- package/dist/usage-allowances.js.map +1 -0
- package/dist/vault-types.d.ts +324 -0
- package/dist/vaults-api.d.ts +65 -0
- package/dist/workspace-artifacts.d.ts +47 -5
- package/dist/workspace-instruction-policies.d.ts +2 -2
- package/dist/workspace-integrations.d.ts +74 -0
- package/dist/workspace-integrations.js +67 -0
- package/dist/workspace-integrations.js.map +1 -0
- package/dist/workspace-state.d.ts +8 -71
- package/package.json +103 -5
- package/src/adapters/express.ts +136 -0
- package/src/adapters/hono.ts +18 -0
- package/src/adapters/next.ts +42 -0
- package/src/allowance-refusal.ts +5 -0
- package/src/app-links.ts +117 -0
- package/src/artifact-catalog.ts +51 -0
- package/src/artifact-client.ts +40 -49
- package/src/artifacts.ts +1 -0
- package/src/automations-client.ts +8 -2
- package/src/azure-live-transport.ts +286 -0
- package/src/browser.ts +37 -0
- package/src/chat/fold.ts +431 -0
- package/src/chat/handler.ts +240 -0
- package/src/chat/http.ts +257 -0
- package/src/chat/ids.ts +85 -0
- package/src/chat/index.ts +88 -0
- package/src/chat/openai.ts +439 -0
- package/src/chat/opengeni.ts +737 -0
- package/src/chat/types.ts +219 -0
- package/src/chat/vercel.ts +204 -0
- package/src/chats.ts +16 -0
- package/src/client.ts +5697 -1833
- package/src/codex-realtime-controller.ts +163 -8
- package/src/codex-realtime-v3-wire.ts +4 -3
- package/src/codex-realtime-v3.ts +12 -3
- package/src/codex-realtime.ts +11 -3
- package/src/company-brain.ts +0 -40
- package/src/core.ts +20 -0
- package/src/deprecation.ts +149 -0
- package/src/document-authority-client.ts +104 -1
- package/src/editable-artifacts/controller.ts +36 -2
- package/src/editable-artifacts/index.ts +1 -0
- package/src/editable-artifacts/types.ts +29 -16
- package/src/embedding-client.ts +519 -0
- package/src/errors.ts +303 -4
- package/src/external-vault-receiver.ts +914 -0
- package/src/feedback.ts +20 -0
- package/src/gateway-realtime-transport.ts +3 -3
- package/src/github-repositories.ts +60 -0
- package/src/index.ts +603 -65
- package/src/insights-usage.ts +64 -0
- package/src/interaction.ts +315 -93
- package/src/knowledge.ts +72 -0
- package/src/known-tools.ts +279 -0
- package/src/message-links.ts +178 -0
- package/src/model-context.ts +113 -0
- package/src/model-display.ts +11 -0
- package/src/model-picker-order.ts +6 -0
- package/src/organization-integration-policy.ts +54 -0
- package/src/pr-review-client.ts +1 -1
- package/src/pr-review.ts +1 -1
- package/src/preference-registry.ts +2 -0
- package/src/prepared-mcp-setup.ts +123 -0
- package/src/proxy.ts +23 -12
- package/src/realtime.ts +15 -6
- package/src/session-history-import.ts +84 -0
- package/src/session-list-entries.ts +1 -0
- package/src/session-message-search.ts +47 -0
- package/src/session-proxy-client.ts +68 -0
- package/src/session-proxy.ts +2098 -0
- package/src/session-titles.ts +8 -82
- package/src/session-usage-limits.ts +71 -0
- package/src/shared-catalog-loader.ts +169 -0
- package/src/site-ai.ts +284 -0
- package/src/site-bridge-host.ts +422 -0
- package/src/site-browser-entry.ts +6 -0
- package/src/site-browser-runtime.gen.ts +2 -0
- package/src/site-data.ts +372 -0
- package/src/site-document.test.ts +64 -0
- package/src/site-document.ts +18 -0
- package/src/site-http.ts +171 -0
- package/src/site-sql.ts +153 -0
- package/src/site-tool-bridge.ts +401 -0
- package/src/site.ts +878 -0
- package/src/skills.ts +69 -0
- package/src/sse.ts +9 -3
- package/src/stream.ts +33 -10
- package/src/tenant-workspaces.ts +148 -0
- package/src/tool-auth.ts +301 -0
- package/src/tool-display-metadata.ts +27 -0
- package/src/tools.ts +452 -0
- package/src/transcription.ts +3 -0
- package/src/types.ts +3170 -582
- package/src/usage-allowances.ts +189 -0
- package/src/vault-types.ts +379 -0
- package/src/vaults-api.ts +339 -0
- package/src/workspace-artifacts.ts +55 -6
- package/src/workspace-instruction-policies.ts +2 -1
- package/src/workspace-integrations.ts +402 -0
- package/src/workspace-state.ts +8 -82
- package/dist/chunk-C2H7HBNP.js.map +0 -1
- package/dist/chunk-C4DJPZRU.js +0 -119
- package/dist/chunk-C4DJPZRU.js.map +0 -1
- package/dist/chunk-JUS2UYRP.js.map +0 -1
- package/dist/chunk-KKRO2VCP.js.map +0 -1
- package/dist/chunk-QT3KFVDP.js.map +0 -1
- package/dist/chunk-TRNI7KEO.js.map +0 -1
- package/dist/chunk-VYCTL62C.js.map +0 -1
- package/dist/chunk-YXQX7RQT.js.map +0 -1
package/src/session-titles.ts
CHANGED
|
@@ -1,84 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
export {
|
|
2
2
|
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
deriveAutomaticSessionTitlePreview,
|
|
4
|
+
deriveSessionDisplayTitle,
|
|
5
|
+
sessionTitleIsPending,
|
|
6
|
+
} from "@opengeni/contracts/session-titles";
|
|
7
|
+
export type {
|
|
8
|
+
SessionDisplayTitleInput,
|
|
9
|
+
SessionDisplayTitleOptions,
|
|
5
10
|
} from "@opengeni/contracts/session-titles";
|
|
6
|
-
import type { Session } from "./types";
|
|
7
|
-
|
|
8
|
-
export { AUTOMATIC_SESSION_TITLE_FALLBACK };
|
|
9
|
-
|
|
10
|
-
// Session creation accepts a body far larger than a navigation label. Bound
|
|
11
|
-
// the source before any replace/split/normalization so a persisted large prompt
|
|
12
|
-
// cannot amplify memory or CPU on every browser render.
|
|
13
|
-
const PROMPT_PREVIEW_SCAN_MAX_CODE_UNITS = 4_096;
|
|
14
|
-
|
|
15
|
-
function deriveOpeningPromptPreview(value: unknown): string | null {
|
|
16
|
-
if (typeof value !== "string") return null;
|
|
17
|
-
|
|
18
|
-
const firstLine = value
|
|
19
|
-
.slice(0, PROMPT_PREVIEW_SCAN_MAX_CODE_UNITS)
|
|
20
|
-
.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, "\n")
|
|
21
|
-
.split(/\n+/u)
|
|
22
|
-
.map((line) => line.trim())
|
|
23
|
-
.find(Boolean);
|
|
24
|
-
if (!firstLine) return null;
|
|
25
|
-
if (containsSensitiveAutomaticSessionTitleValue(firstLine)) return null;
|
|
26
|
-
|
|
27
|
-
const preview = boundAutomaticSessionTitle(firstLine.replace(/\s+/gu, " "))
|
|
28
|
-
.replace(/[\s.!?,;:\-–—]+$/u, "")
|
|
29
|
-
.trim();
|
|
30
|
-
if (!preview) return null;
|
|
31
|
-
return preview;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type SessionDisplayTitleInput = {
|
|
35
|
-
title?: Session["title"] | undefined;
|
|
36
|
-
titleSource?: Session["titleSource"] | undefined;
|
|
37
|
-
initialMessage?: Session["initialMessage"] | null | undefined;
|
|
38
|
-
metadata?: Readonly<Record<string, unknown>> | undefined;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type SessionDisplayTitleOptions = {
|
|
42
|
-
/** Optional metadata fields to try before the opening-prompt preview. */
|
|
43
|
-
metadataKeys?: readonly string[] | undefined;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Whether the durable title still represents the automatic-title pending state.
|
|
48
|
-
* A user-authored title always wins, even when its literal value is the fallback.
|
|
49
|
-
*/
|
|
50
|
-
export function sessionTitleIsPending(input: SessionDisplayTitleInput): boolean {
|
|
51
|
-
const title = input.title?.trim() ?? "";
|
|
52
|
-
return input.titleSource !== "user" && (!title || title === AUTOMATIC_SESSION_TITLE_FALLBACK);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Derive the title a client should display for a session.
|
|
57
|
-
*
|
|
58
|
-
* A semantic agent title or human rename wins. While automatic naming is still
|
|
59
|
-
* pending, clients show a short, sensitive-safe preview of the opening prompt;
|
|
60
|
-
* the preview is never persisted as title metadata and is replaced naturally
|
|
61
|
-
* when `session.title_set` arrives. Obvious credential-, URL-, or identifier-
|
|
62
|
-
* shaped prompt prefixes retain the generic fallback.
|
|
63
|
-
*/
|
|
64
|
-
export function deriveSessionDisplayTitle(
|
|
65
|
-
input: SessionDisplayTitleInput,
|
|
66
|
-
options: SessionDisplayTitleOptions = {},
|
|
67
|
-
): string {
|
|
68
|
-
const title = input.title?.trim() ?? "";
|
|
69
|
-
if (input.titleSource === "user") {
|
|
70
|
-
return title || AUTOMATIC_SESSION_TITLE_FALLBACK;
|
|
71
|
-
}
|
|
72
|
-
if (title && !sessionTitleIsPending(input)) {
|
|
73
|
-
return title;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
for (const key of options.metadataKeys ?? []) {
|
|
77
|
-
const value = input.metadata?.[key];
|
|
78
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
79
|
-
return value.trim();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return deriveOpeningPromptPreview(input.initialMessage) ?? AUTOMATIC_SESSION_TITLE_FALLBACK;
|
|
84
|
-
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @opengeni/sdk/session-usage-limits — per-task usage limit helpers that are
|
|
2
|
+
// safe in browsers and React Native. Reads and writes use the root client's
|
|
3
|
+
// `getSessionUsageLimit` / `setSessionUsageLimit`, or these free functions over
|
|
4
|
+
// any client's `requestJson`.
|
|
5
|
+
//
|
|
6
|
+
// Amounts are integer USD micros of API-equivalent model usage (the
|
|
7
|
+
// pre-margin provider list-price estimate), not your bill. The limit is
|
|
8
|
+
// checked before each new model request: requests already running can take
|
|
9
|
+
// recorded usage above it. See docs/session-usage-limits.md.
|
|
10
|
+
import type { OpenGeniClient, OpenGeniRequestOptions } from "./client";
|
|
11
|
+
import { sessionUsageLimitNotConfirmed } from "./errors";
|
|
12
|
+
import type {
|
|
13
|
+
SessionUsageLimit,
|
|
14
|
+
SetSessionUsageLimitRequest,
|
|
15
|
+
} from "@opengeni/contracts/session-usage-limits";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
SessionUsageLimit,
|
|
19
|
+
SessionUsageLimitHistory,
|
|
20
|
+
SessionUsageLimitReachedRefusal,
|
|
21
|
+
SessionUsageLimitStatus,
|
|
22
|
+
SetSessionUsageLimitRequest,
|
|
23
|
+
} from "@opengeni/contracts/session-usage-limits";
|
|
24
|
+
export {
|
|
25
|
+
formatSessionUsageMicros,
|
|
26
|
+
parseSessionUsageLimitReachedRefusal,
|
|
27
|
+
SESSION_USAGE_LIMIT_GOAL_PAUSED_REASON,
|
|
28
|
+
SESSION_USAGE_LIMIT_SEGMENT,
|
|
29
|
+
sessionUsageLimitReachedMessage,
|
|
30
|
+
} from "@opengeni/contracts/session-usage-limit-refusal";
|
|
31
|
+
export type { ParsedSessionUsageLimitReachedRefusal } from "@opengeni/contracts/session-usage-limit-refusal";
|
|
32
|
+
|
|
33
|
+
type RequestJsonClient = Pick<OpenGeniClient, "requestJson">;
|
|
34
|
+
|
|
35
|
+
function usageLimitPath(workspaceId: string, sessionId: string): string {
|
|
36
|
+
return `/v1/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/usage-limit`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read a task's usage limit state (a child returns its root's, `inherited: true`). */
|
|
40
|
+
export async function getSessionUsageLimit(
|
|
41
|
+
client: RequestJsonClient,
|
|
42
|
+
workspaceId: string,
|
|
43
|
+
sessionId: string,
|
|
44
|
+
options: OpenGeniRequestOptions = {},
|
|
45
|
+
): Promise<SessionUsageLimit> {
|
|
46
|
+
return await client.requestJson<SessionUsageLimit>(
|
|
47
|
+
"GET",
|
|
48
|
+
usageLimitPath(workspaceId, sessionId),
|
|
49
|
+
undefined,
|
|
50
|
+
{},
|
|
51
|
+
options,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Compare-and-set a root task's limit; `limitMicros: null` clears it. Never resumes work. */
|
|
56
|
+
export async function setSessionUsageLimit(
|
|
57
|
+
client: RequestJsonClient,
|
|
58
|
+
workspaceId: string,
|
|
59
|
+
sessionId: string,
|
|
60
|
+
request: SetSessionUsageLimitRequest,
|
|
61
|
+
): Promise<SessionUsageLimit> {
|
|
62
|
+
const committed = await client.requestJson<SessionUsageLimit>(
|
|
63
|
+
"PUT",
|
|
64
|
+
usageLimitPath(workspaceId, sessionId),
|
|
65
|
+
request,
|
|
66
|
+
);
|
|
67
|
+
if (committed.limitMicros !== request.limitMicros) {
|
|
68
|
+
throw sessionUsageLimitNotConfirmed(sessionId);
|
|
69
|
+
}
|
|
70
|
+
return committed;
|
|
71
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight loader for one digest-pinned tool catalog.
|
|
3
|
+
*
|
|
4
|
+
* - Concurrent cold callers share one request instead of each fetching.
|
|
5
|
+
* - A waiter's AbortSignal cancels only its own wait. The shared request is
|
|
6
|
+
* cancelled once every waiter has aborted, and the next caller then starts
|
|
7
|
+
* a new request. A waiter without a signal keeps the shared request alive
|
|
8
|
+
* for every joiner until it settles (bounded by transport/server timeouts).
|
|
9
|
+
* - A failed load is never cached; the next caller starts a new request.
|
|
10
|
+
* - An explicit refresh, or a stale rejection, always obtains a catalog from a
|
|
11
|
+
* request started after that refresh/rejection. Callers rejected on the same
|
|
12
|
+
* stale digest share one post-rejection request while it is still current.
|
|
13
|
+
* - A load started before an invalidation never overwrites the newer state.
|
|
14
|
+
*/
|
|
15
|
+
export type SharedCatalogLoader<T extends { digest: string }> = {
|
|
16
|
+
load(options?: { refresh?: boolean; signal?: AbortSignal }): Promise<T>;
|
|
17
|
+
/** Reload after the server rejected `staleDigest` as a stale catalog. */
|
|
18
|
+
reloadAfterStale(staleDigest: string, signal?: AbortSignal): Promise<T>;
|
|
19
|
+
/** Drop the cached catalog when it is `digest`, without fetching. */
|
|
20
|
+
invalidate(digest: string): void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SharedLoad<T> = {
|
|
24
|
+
epoch: number;
|
|
25
|
+
promise: Promise<T>;
|
|
26
|
+
controller: AbortController;
|
|
27
|
+
waiters: number;
|
|
28
|
+
settled: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function createSharedCatalogLoader<T extends { digest: string }>(
|
|
32
|
+
/**
|
|
33
|
+
* `refresh` is true when this load follows an invalidation or explicit
|
|
34
|
+
* refresh. `signal` aborts only when every waiter has abandoned the load.
|
|
35
|
+
*/
|
|
36
|
+
fetchCatalog: (refresh: boolean, signal: AbortSignal) => Promise<T>,
|
|
37
|
+
): SharedCatalogLoader<T> {
|
|
38
|
+
let snapshot: T | null = null;
|
|
39
|
+
let epoch = 0;
|
|
40
|
+
let refreshPending = false;
|
|
41
|
+
let inFlight: SharedLoad<T> | null = null;
|
|
42
|
+
let staleReload: {
|
|
43
|
+
staleDigest: string;
|
|
44
|
+
load: SharedLoad<T>;
|
|
45
|
+
settled: T | null;
|
|
46
|
+
} | null = null;
|
|
47
|
+
|
|
48
|
+
const start = (): SharedLoad<T> => {
|
|
49
|
+
if (inFlight && inFlight.epoch === epoch) return inFlight;
|
|
50
|
+
const refresh = refreshPending;
|
|
51
|
+
const load = {
|
|
52
|
+
epoch,
|
|
53
|
+
controller: new AbortController(),
|
|
54
|
+
waiters: 0,
|
|
55
|
+
settled: false,
|
|
56
|
+
} as SharedLoad<T>;
|
|
57
|
+
inFlight = load;
|
|
58
|
+
load.promise = (async () => {
|
|
59
|
+
try {
|
|
60
|
+
const next = await fetchCatalog(refresh, load.controller.signal);
|
|
61
|
+
// An abandoned load (every waiter aborted) never writes shared state,
|
|
62
|
+
// even when its transport ignored the abort and settled late.
|
|
63
|
+
if (load.epoch === epoch && !load.controller.signal.aborted) {
|
|
64
|
+
snapshot = next;
|
|
65
|
+
refreshPending = false;
|
|
66
|
+
}
|
|
67
|
+
return next;
|
|
68
|
+
} finally {
|
|
69
|
+
load.settled = true;
|
|
70
|
+
if (inFlight === load) inFlight = null;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
// Every waiter observes rejection itself; never leave an unobserved one.
|
|
74
|
+
load.promise.catch(() => undefined);
|
|
75
|
+
return load;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const abandon = (load: SharedLoad<T>, reason: unknown): void => {
|
|
79
|
+
if (inFlight === load) inFlight = null;
|
|
80
|
+
if (staleReload?.load === load) staleReload = null;
|
|
81
|
+
load.controller.abort(reason);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const wait = (load: SharedLoad<T>, signal: AbortSignal | undefined): Promise<T> => {
|
|
85
|
+
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
86
|
+
load.waiters += 1;
|
|
87
|
+
return new Promise<T>((resolve, reject) => {
|
|
88
|
+
let released = false;
|
|
89
|
+
const release = (): boolean => {
|
|
90
|
+
if (released) return false;
|
|
91
|
+
released = true;
|
|
92
|
+
load.waiters -= 1;
|
|
93
|
+
signal?.removeEventListener("abort", onAbort);
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
const onAbort = () => {
|
|
97
|
+
if (!release()) return;
|
|
98
|
+
const reason = abortReason(signal!);
|
|
99
|
+
reject(reason);
|
|
100
|
+
if (load.waiters === 0 && !load.settled) abandon(load, reason);
|
|
101
|
+
};
|
|
102
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
load.promise.then(
|
|
104
|
+
(value) => {
|
|
105
|
+
if (release()) resolve(value);
|
|
106
|
+
},
|
|
107
|
+
(error: unknown) => {
|
|
108
|
+
if (release()) reject(error);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const advance = (): void => {
|
|
115
|
+
snapshot = null;
|
|
116
|
+
epoch += 1;
|
|
117
|
+
refreshPending = true;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
async load(options = {}) {
|
|
122
|
+
options.signal?.throwIfAborted();
|
|
123
|
+
if (options.refresh) advance();
|
|
124
|
+
if (snapshot) return snapshot;
|
|
125
|
+
return await wait(start(), options.signal);
|
|
126
|
+
},
|
|
127
|
+
async reloadAfterStale(staleDigest, signal) {
|
|
128
|
+
signal?.throwIfAborted();
|
|
129
|
+
// Share a post-rejection reload while it is still the current load, or
|
|
130
|
+
// while its result is still the current catalog; never reuse a
|
|
131
|
+
// superseded load or result.
|
|
132
|
+
const shared = staleReload;
|
|
133
|
+
if (
|
|
134
|
+
shared?.staleDigest === staleDigest &&
|
|
135
|
+
(shared.settled === null ? shared.load.epoch === epoch : shared.settled === snapshot)
|
|
136
|
+
) {
|
|
137
|
+
return await wait(shared.load, signal);
|
|
138
|
+
}
|
|
139
|
+
advance();
|
|
140
|
+
const load = start();
|
|
141
|
+
const reload: { staleDigest: string; load: SharedLoad<T>; settled: T | null } = {
|
|
142
|
+
staleDigest,
|
|
143
|
+
load,
|
|
144
|
+
settled: null,
|
|
145
|
+
};
|
|
146
|
+
staleReload = reload;
|
|
147
|
+
load.promise.then(
|
|
148
|
+
(next) => {
|
|
149
|
+
// The same digest again proves nothing newer; a later rejection must
|
|
150
|
+
// start its own request rather than reuse this one.
|
|
151
|
+
if (staleReload !== reload) return;
|
|
152
|
+
if (next.digest === staleDigest) staleReload = null;
|
|
153
|
+
else reload.settled = next;
|
|
154
|
+
},
|
|
155
|
+
() => {
|
|
156
|
+
if (staleReload === reload) staleReload = null;
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
return await wait(load, signal);
|
|
160
|
+
},
|
|
161
|
+
invalidate(digest) {
|
|
162
|
+
if (snapshot?.digest === digest) advance();
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
168
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
169
|
+
}
|
package/src/site-ai.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { OpenGeniClient } from "./client";
|
|
2
|
+
import { ChatTurnFold, asRecord, type ChatTurnTerminal } from "./chat/fold";
|
|
3
|
+
import { sessionEventStreamCoveredThrough } from "./stream";
|
|
4
|
+
import type { CreateSessionRequest, Session, SessionEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
/** App-authored input, unchanged: no built-in tasks, prompt templates or UI. */
|
|
7
|
+
export type OpenGeniSiteAiRequest = Pick<
|
|
8
|
+
CreateSessionRequest,
|
|
9
|
+
"initialMessage" | "idempotencyKey" | "model" | "reasoningEffort" | "latencyMode"
|
|
10
|
+
> & { initialMessage: string };
|
|
11
|
+
|
|
12
|
+
export type OpenGeniSiteAiResult = {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
turnId: string | null;
|
|
15
|
+
text: string;
|
|
16
|
+
status: "completed" | "pending" | "cancelled";
|
|
17
|
+
/** Cursor for subsequent output. Resuming after it returns a suffix, not earlier text. */
|
|
18
|
+
after: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type OpenGeniSiteAiOptions = {
|
|
22
|
+
/** Stops observing, not the underlying work. Use the ordinary session control API to cancel. */
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** Retain the handle before waiting for output, including when observation is interrupted. */
|
|
25
|
+
onSession?: (sessionId: string) => void;
|
|
26
|
+
/** Append-only answer text, folded by the same SDK implementation as embedded chat. */
|
|
27
|
+
onText?: (text: string) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type OpenGeniSiteAi = {
|
|
31
|
+
/** Starts an ordinary viewer-owned Site session and returns its first turn's output. */
|
|
32
|
+
run(
|
|
33
|
+
request: OpenGeniSiteAiRequest,
|
|
34
|
+
options?: OpenGeniSiteAiOptions,
|
|
35
|
+
): Promise<OpenGeniSiteAiResult>;
|
|
36
|
+
/** Observe without sending work. Omit `after` to replay the complete answer;
|
|
37
|
+
* with `after`, text/onText contain only subsequent output. Pass the retained
|
|
38
|
+
* turnId when resuming a pending turn, so another turn cannot settle it. */
|
|
39
|
+
result(
|
|
40
|
+
sessionId: string,
|
|
41
|
+
options?: OpenGeniSiteAiOptions & { after?: number; turnId?: string },
|
|
42
|
+
): Promise<OpenGeniSiteAiResult>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Native create, session state and replayable events; the host remains the authority. */
|
|
46
|
+
export function createOpenGeniSiteAi(
|
|
47
|
+
client: Pick<OpenGeniClient, "createSession" | "getSession" | "listEventPage" | "streamEvents">,
|
|
48
|
+
workspaceId: string,
|
|
49
|
+
): OpenGeniSiteAi {
|
|
50
|
+
const result: OpenGeniSiteAi["result"] = async (sessionId, options = {}) => {
|
|
51
|
+
options.signal?.throwIfAborted();
|
|
52
|
+
const upstream = new AbortController();
|
|
53
|
+
const onAbort = () => upstream.abort(options.signal?.reason);
|
|
54
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
55
|
+
const cutoff = options.after ?? 0;
|
|
56
|
+
let after = 0;
|
|
57
|
+
let text = "";
|
|
58
|
+
let fold =
|
|
59
|
+
options.turnId || cutoff === 0
|
|
60
|
+
? new ChatTurnFold(workspaceId, sessionId, options.turnId ?? null)
|
|
61
|
+
: null;
|
|
62
|
+
// Without a retained turn id, select the first turn after the cursor, as
|
|
63
|
+
// live observation does. Keep the prefix only until that turn is known.
|
|
64
|
+
let prefix: SessionEvent[] = [];
|
|
65
|
+
let terminal: ChatTurnTerminal | null = null;
|
|
66
|
+
const pending: { event: SessionEvent | null } = { event: null };
|
|
67
|
+
let stream: ReturnType<OpenGeniClient["streamEvents"]> | undefined;
|
|
68
|
+
let streamAbort: AbortController | undefined;
|
|
69
|
+
const closeStream = () => {
|
|
70
|
+
streamAbort?.abort();
|
|
71
|
+
// Some fetch implementations await request abort in iterator.return().
|
|
72
|
+
// Abort first, and never make result delivery depend on its unwinding.
|
|
73
|
+
if (stream) void Promise.resolve(stream.return(undefined)).catch(() => undefined);
|
|
74
|
+
stream = undefined;
|
|
75
|
+
streamAbort = undefined;
|
|
76
|
+
};
|
|
77
|
+
const onUpstreamAbort = () => streamAbort?.abort(upstream.signal.reason);
|
|
78
|
+
upstream.signal.addEventListener("abort", onUpstreamAbort);
|
|
79
|
+
const emit = (chunks: ReturnType<ChatTurnFold["push"]>["chunks"]) => {
|
|
80
|
+
for (const chunk of chunks)
|
|
81
|
+
if (chunk.type === "text") {
|
|
82
|
+
upstream.signal.throwIfAborted();
|
|
83
|
+
text += chunk.text;
|
|
84
|
+
options.onText?.(chunk.text);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const selectTurn = (turnId: string | null) => {
|
|
88
|
+
fold = new ChatTurnFold(workspaceId, sessionId, turnId);
|
|
89
|
+
for (const event of prefix) {
|
|
90
|
+
const step = fold.push(event);
|
|
91
|
+
if (step.terminal && step.terminal !== "pending") terminal = step.terminal;
|
|
92
|
+
}
|
|
93
|
+
prefix = [];
|
|
94
|
+
};
|
|
95
|
+
const consume = (event: SessionEvent) => {
|
|
96
|
+
const covered = eventCursor(event);
|
|
97
|
+
if (covered <= after) return;
|
|
98
|
+
after = covered;
|
|
99
|
+
if (!fold) {
|
|
100
|
+
if (covered <= cutoff) {
|
|
101
|
+
prefix.push(event);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!isTurnOutput(event) && !isWait(event)) return;
|
|
105
|
+
selectTurn(event.turnId ?? lastTurnId(prefix));
|
|
106
|
+
}
|
|
107
|
+
const current = fold!;
|
|
108
|
+
const sameTurn = !event.turnId || !current.turnId || event.turnId === current.turnId;
|
|
109
|
+
if (sameTurn && isWait(event)) {
|
|
110
|
+
if (!current.turnId && event.turnId) current.turnId = event.turnId;
|
|
111
|
+
if (event.type !== "session.status.changed") pending.event = event;
|
|
112
|
+
// A historical wait is not a settlement. Only fold it here when
|
|
113
|
+
// rebuilding text the caller already observed before their cursor.
|
|
114
|
+
if (covered > cutoff) return;
|
|
115
|
+
}
|
|
116
|
+
const step = current.push(event);
|
|
117
|
+
if (covered > cutoff) emit(step.chunks);
|
|
118
|
+
if (step.terminal && step.terminal !== "pending") terminal = step.terminal;
|
|
119
|
+
};
|
|
120
|
+
const reply = (status: ChatTurnTerminal): OpenGeniSiteAiResult => {
|
|
121
|
+
upstream.signal.throwIfAborted();
|
|
122
|
+
if (status === "failed") throw fold!.failureError();
|
|
123
|
+
return {
|
|
124
|
+
sessionId,
|
|
125
|
+
turnId: fold?.turnId ?? null,
|
|
126
|
+
text,
|
|
127
|
+
status:
|
|
128
|
+
status === "pending" ? "pending" : status === "cancelled" ? "cancelled" : "completed",
|
|
129
|
+
after: Math.max(cutoff, after),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const readSession = () =>
|
|
133
|
+
observe(
|
|
134
|
+
() => client.getSession(workspaceId, sessionId, { fresh: true, signal: upstream.signal }),
|
|
135
|
+
upstream.signal,
|
|
136
|
+
);
|
|
137
|
+
const replayThrough = async (through: number) => {
|
|
138
|
+
while (after < through) {
|
|
139
|
+
const previous = after;
|
|
140
|
+
const page = await observe(
|
|
141
|
+
() =>
|
|
142
|
+
client.listEventPage(workspaceId, sessionId, {
|
|
143
|
+
after,
|
|
144
|
+
before: through + 1,
|
|
145
|
+
direction: "after",
|
|
146
|
+
limit: 200,
|
|
147
|
+
mode: "forensic",
|
|
148
|
+
payloadMode: "full",
|
|
149
|
+
}),
|
|
150
|
+
upstream.signal,
|
|
151
|
+
);
|
|
152
|
+
for (const event of page.events) {
|
|
153
|
+
consume(event);
|
|
154
|
+
if (terminal && after > cutoff) return;
|
|
155
|
+
}
|
|
156
|
+
// Byte-limited short pages are not EOF; follow durable coverage.
|
|
157
|
+
after = Math.max(after, page.nextAfter ?? page.coveredSequence?.last ?? 0);
|
|
158
|
+
if (after <= previous)
|
|
159
|
+
throw new Error("The Site AI event replay did not reach the session cursor.");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const waiting = (session: Session) =>
|
|
163
|
+
session.status === "requires_action" &&
|
|
164
|
+
(!session.activeTurnId || !fold?.turnId || session.activeTurnId === fold.turnId);
|
|
165
|
+
try {
|
|
166
|
+
let session = await readSession();
|
|
167
|
+
// Reconstruct the exact prefix separately: unlike SSE coalescing, this
|
|
168
|
+
// bounded native page read cannot merge output from both sides of cutoff.
|
|
169
|
+
await replayThrough(Math.min(cutoff, session.lastSequence));
|
|
170
|
+
if (terminal) return reply(terminal);
|
|
171
|
+
for (;;) {
|
|
172
|
+
await replayThrough(session.lastSequence);
|
|
173
|
+
if (terminal) return reply(terminal);
|
|
174
|
+
if (waiting(session)) {
|
|
175
|
+
const fresh = await readSession();
|
|
176
|
+
if (fresh.lastSequence > after) {
|
|
177
|
+
session = fresh;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (waiting(fresh)) {
|
|
181
|
+
if (!fold) selectTurn(fresh.activeTurnId ?? lastTurnId(prefix));
|
|
182
|
+
else if (!fold.turnId) fold.turnId = fresh.activeTurnId ?? null;
|
|
183
|
+
if (pending.event && eventCursor(pending.event) > cutoff)
|
|
184
|
+
emit(fold!.push(pending.event).chunks);
|
|
185
|
+
return reply("pending");
|
|
186
|
+
}
|
|
187
|
+
session = fresh;
|
|
188
|
+
}
|
|
189
|
+
streamAbort = new AbortController();
|
|
190
|
+
stream = client.streamEvents(workspaceId, sessionId, {
|
|
191
|
+
after: Math.max(cutoff, after),
|
|
192
|
+
signal: streamAbort.signal,
|
|
193
|
+
});
|
|
194
|
+
for (;;) {
|
|
195
|
+
const next = await observe(() => stream!.next(), upstream.signal);
|
|
196
|
+
if (next.done) throw new Error("The Site AI event stream ended before the turn settled.");
|
|
197
|
+
consume(next.value);
|
|
198
|
+
if (terminal) return reply(terminal);
|
|
199
|
+
if (isWait(next.value) && (!next.value.turnId || next.value.turnId === fold?.turnId)) {
|
|
200
|
+
// Reconcile the finite durable tail before declaring a live wait.
|
|
201
|
+
// Reopen from its cursor so buffered SSE overlap cannot repeat text.
|
|
202
|
+
closeStream();
|
|
203
|
+
session = await readSession();
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
210
|
+
upstream.abort();
|
|
211
|
+
closeStream();
|
|
212
|
+
upstream.signal.removeEventListener("abort", onUpstreamAbort);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
async run(request, options = {}) {
|
|
217
|
+
options.signal?.throwIfAborted();
|
|
218
|
+
// Pick the message fields rather than forwarding a caller's extra runtime
|
|
219
|
+
// properties. Tools, ownership, credentials and limits belong to the host.
|
|
220
|
+
const session = await client.createSession(workspaceId, {
|
|
221
|
+
initialMessage: request.initialMessage,
|
|
222
|
+
idempotencyKey: request.idempotencyKey ?? crypto.randomUUID(),
|
|
223
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
224
|
+
...(request.reasoningEffort === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: { reasoningEffort: request.reasoningEffort }),
|
|
227
|
+
...(request.latencyMode === undefined ? {} : { latencyMode: request.latencyMode }),
|
|
228
|
+
});
|
|
229
|
+
options.onSession?.(session.id);
|
|
230
|
+
return await result(session.id, options);
|
|
231
|
+
},
|
|
232
|
+
result,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isWait(event: SessionEvent): boolean {
|
|
237
|
+
return (
|
|
238
|
+
event.type === "session.requiresAction" ||
|
|
239
|
+
event.type === "session.humanInput.requested" ||
|
|
240
|
+
(event.type === "session.status.changed" &&
|
|
241
|
+
asRecord(event.payload).status === "requires_action")
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isTurnOutput(event: SessionEvent): boolean {
|
|
246
|
+
return (
|
|
247
|
+
event.type.startsWith("turn.") ||
|
|
248
|
+
event.type.startsWith("agent.") ||
|
|
249
|
+
event.type === "user.message"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function lastTurnId(events: SessionEvent[]): string | null {
|
|
254
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
255
|
+
const event = events[index]!;
|
|
256
|
+
if (event.turnId && (isTurnOutput(event) || isWait(event))) return event.turnId;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function eventCursor(event: SessionEvent): number {
|
|
262
|
+
return Math.max(
|
|
263
|
+
event.sequence,
|
|
264
|
+
event.coveredThrough ?? 0,
|
|
265
|
+
sessionEventStreamCoveredThrough(event) ?? 0,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Event-page reads have no signal option; observation can still stop immediately. */
|
|
270
|
+
async function observe<T>(read: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
271
|
+
signal.throwIfAborted();
|
|
272
|
+
let onAbort: () => void = () => {};
|
|
273
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
274
|
+
onAbort = () => reject(signal.reason);
|
|
275
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
const value = await Promise.race([read(), aborted]);
|
|
279
|
+
signal.throwIfAborted();
|
|
280
|
+
return value;
|
|
281
|
+
} finally {
|
|
282
|
+
signal.removeEventListener("abort", onAbort);
|
|
283
|
+
}
|
|
284
|
+
}
|