@opengeni/sdk 3.1.0 → 3.1.2
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 +1037 -71
- package/dist/accounts.d.ts +5 -1
- package/dist/accounts.js +13 -1
- package/dist/accounts.js.map +1 -1
- 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 +1703 -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-6HZGAIUE.js +1665 -0
- package/dist/chunk-6HZGAIUE.js.map +1 -0
- package/dist/chunk-7EWT3PV6.js +368 -0
- package/dist/chunk-7EWT3PV6.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-JXXCTW5W.js +1392 -0
- package/dist/chunk-JXXCTW5W.js.map +1 -0
- package/dist/{chunk-YTAWBDO2.js → chunk-KADZ5PGG.js} +28 -2
- package/dist/chunk-KADZ5PGG.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-7ZXBPJZP.js → chunk-NEM5N6C6.js} +3902 -1457
- package/dist/chunk-NEM5N6C6.js.map +1 -0
- package/dist/{chunk-GW3EJ2GP.js → chunk-O7KANXC3.js} +86 -13
- package/dist/chunk-O7KANXC3.js.map +1 -0
- package/dist/{chunk-VUQZAB3O.js → chunk-OAKHWFSV.js} +28 -32
- package/dist/chunk-OAKHWFSV.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-RJHCBZF7.js +270 -0
- package/dist/chunk-RJHCBZF7.js.map +1 -0
- package/dist/chunk-S3D75WSQ.js +340 -0
- package/dist/chunk-S3D75WSQ.js.map +1 -0
- package/dist/chunk-UM4ZXXJR.js +9 -0
- package/dist/chunk-UM4ZXXJR.js.map +1 -0
- package/dist/chunk-VPVMLM7Q.js +15 -0
- package/dist/chunk-VPVMLM7Q.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-XA7HVIHI.js +126 -0
- package/dist/chunk-XA7HVIHI.js.map +1 -0
- package/dist/{chunk-TX3EIENU.js → chunk-YDVPI7WT.js} +73 -2
- package/dist/chunk-YDVPI7WT.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 +1065 -124
- 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/company-profile.d.ts +13 -0
- 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/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 +64 -11
- package/dist/index.js +566 -141
- 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 +103 -71
- package/dist/interaction.js +4 -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 +4 -2
- 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 -0
- 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-bridge-host.d.ts +112 -0
- package/dist/site-bridge-host.js +285 -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 +182 -0
- package/dist/site.js +62 -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 +2848 -573
- 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 +236 -0
- package/dist/vaults-api.d.ts +57 -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 +65 -0
- package/dist/workspace-integrations.js.map +1 -0
- package/dist/workspace-state.d.ts +8 -71
- package/package.json +99 -5
- package/src/accounts.ts +18 -0
- 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 +5822 -1847
- 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/company-profile.ts +13 -0
- 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/feedback.ts +20 -0
- package/src/gateway-realtime-transport.ts +3 -3
- package/src/github-repositories.ts +60 -0
- package/src/index.ts +611 -64
- package/src/insights-usage.ts +61 -0
- package/src/interaction.ts +274 -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 +16 -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 +10 -0
- package/src/session-usage-limits.ts +71 -0
- package/src/shared-catalog-loader.ts +169 -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 +867 -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 +3216 -581
- package/src/usage-allowances.ts +189 -0
- package/src/vault-types.ts +293 -0
- package/src/vaults-api.ts +288 -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-7ZXBPJZP.js.map +0 -1
- 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-GW3EJ2GP.js.map +0 -1
- package/dist/chunk-TX3EIENU.js.map +0 -1
- package/dist/chunk-VUQZAB3O.js.map +0 -1
- package/dist/chunk-VYCTL62C.js.map +0 -1
- package/dist/chunk-YTAWBDO2.js.map +0 -1
package/src/chat/fold.ts
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import type { SessionEvent } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
EMPTY_FINAL_REPLY_NOTICE,
|
|
4
|
+
turnCompletedWithEmptyFinalReply,
|
|
5
|
+
} from "@opengeni/contracts/session-final-reply";
|
|
6
|
+
import { OpenGeniChatError, type ChatChunk, type ChatPending, type ChatReply } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Event folding shared by `send`, `stream`, and every protocol adapter.
|
|
10
|
+
*
|
|
11
|
+
* One fold covers one logical turn: text deltas accumulate into message
|
|
12
|
+
* segments (a tool call closes the current segment, the completed event
|
|
13
|
+
* reconciles it), tool call/output pairs become tool chunks, and the turn
|
|
14
|
+
* settles on a terminal event or a human wait. Events that carry a different
|
|
15
|
+
* turn id than the expected one are ignored so a queued follow-up never ends
|
|
16
|
+
* early on the previous turn's settlement.
|
|
17
|
+
*
|
|
18
|
+
* A segment belongs to one provider message when events carry `messageId`: a
|
|
19
|
+
* delta for another message starts a new segment, and a completion reconciles
|
|
20
|
+
* only the segment of its own message. Completions may arrive after later
|
|
21
|
+
* messages already streamed, so matching by position would let one message's
|
|
22
|
+
* completion close another's segment and repeat its text.
|
|
23
|
+
*
|
|
24
|
+
* Commentary (`phase: "commentary"`) narrates the work, like a tool chunk, so
|
|
25
|
+
* it is held back from the reply text. Only a turn that settles without any
|
|
26
|
+
* answer text falls back to its latest commentary, emitted at settlement.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type ChatTurnTerminal = "completed" | "failed" | "cancelled" | "pending";
|
|
30
|
+
|
|
31
|
+
export type ChatFoldStep = {
|
|
32
|
+
chunks: ChatChunk[];
|
|
33
|
+
terminal: ChatTurnTerminal | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Incremental replay of unresolved requests, without retaining the event log. */
|
|
37
|
+
export class ChatPendingFold {
|
|
38
|
+
private readonly requests = new Map<string, { pending: ChatPending; turnId: string | null }>();
|
|
39
|
+
|
|
40
|
+
push(event: SessionEvent): void {
|
|
41
|
+
const payload = asRecord(event.payload);
|
|
42
|
+
if (event.type === "session.requiresAction") {
|
|
43
|
+
for (const [key, request] of this.requests) {
|
|
44
|
+
if (request.pending.kind === "approval") this.requests.delete(key);
|
|
45
|
+
}
|
|
46
|
+
for (const approval of Array.isArray(payload.approvals) ? payload.approvals : []) {
|
|
47
|
+
const pending = approvalPending({ approvals: [approval] });
|
|
48
|
+
if (pending) this.put(pending, event.turnId ?? null);
|
|
49
|
+
}
|
|
50
|
+
} else if (event.type === "session.humanInput.requested") {
|
|
51
|
+
const pending = humanInputPending(payload);
|
|
52
|
+
if (pending) this.put(pending, event.turnId ?? null);
|
|
53
|
+
} else if (event.type === "user.approvalDecision") {
|
|
54
|
+
this.requests.delete(`approval:${payload.approvalId}`);
|
|
55
|
+
} else if (event.type === "user.humanInputResponse") {
|
|
56
|
+
this.requests.delete(`human_input:${payload.requestId}`);
|
|
57
|
+
} else if (["turn.completed", "turn.failed", "turn.cancelled"].includes(event.type)) {
|
|
58
|
+
for (const [key, request] of this.requests) {
|
|
59
|
+
if (request.turnId === null || event.turnId == null || request.turnId === event.turnId) {
|
|
60
|
+
this.requests.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pending(now = Date.now()): ChatPending[] {
|
|
67
|
+
return [...this.requests.values()]
|
|
68
|
+
.map((request) => request.pending)
|
|
69
|
+
.filter((pending) => {
|
|
70
|
+
if (pending.kind !== "human_input") return true;
|
|
71
|
+
const expiresAt = asRecord(pending.payload).expiresAt;
|
|
72
|
+
return expiresAt == null || (typeof expiresAt === "string" && Date.parse(expiresAt) > now);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private put(pending: ChatPending, turnId: string | null): void {
|
|
77
|
+
this.requests.set(`${pending.kind}:${pending.requestId}`, { pending, turnId });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Segment = {
|
|
82
|
+
text: string;
|
|
83
|
+
open: boolean;
|
|
84
|
+
commentary: boolean;
|
|
85
|
+
/** Provider message identity; `null` for events that carry none. */
|
|
86
|
+
messageId: string | null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function noStep(): ChatFoldStep {
|
|
90
|
+
return { chunks: [], terminal: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class ChatTurnFold {
|
|
94
|
+
readonly events: SessionEvent[] = [];
|
|
95
|
+
turnId: string | null;
|
|
96
|
+
pending: ChatPending | null = null;
|
|
97
|
+
failure: SessionEvent | null = null;
|
|
98
|
+
private emptyFinalReply = false;
|
|
99
|
+
private readonly segments: Segment[] = [];
|
|
100
|
+
private readonly openTools = new Map<string, string>();
|
|
101
|
+
|
|
102
|
+
constructor(
|
|
103
|
+
private readonly workspaceId: string,
|
|
104
|
+
private readonly sessionId: string,
|
|
105
|
+
expectedTurnId: string | null,
|
|
106
|
+
) {
|
|
107
|
+
this.turnId = expectedTurnId;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get text(): string {
|
|
111
|
+
return this.segments
|
|
112
|
+
.filter((segment) => !segment.commentary)
|
|
113
|
+
.map((segment) => segment.text)
|
|
114
|
+
.filter((text) => text.length > 0)
|
|
115
|
+
.join("\n\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
push(event: SessionEvent): ChatFoldStep {
|
|
119
|
+
const eventTurnId = typeof event.turnId === "string" ? event.turnId : null;
|
|
120
|
+
if (this.turnId && eventTurnId && eventTurnId !== this.turnId) {
|
|
121
|
+
return { chunks: [], terminal: null };
|
|
122
|
+
}
|
|
123
|
+
if (!this.turnId && eventTurnId && isTurnScopedType(event.type)) {
|
|
124
|
+
this.turnId = eventTurnId;
|
|
125
|
+
}
|
|
126
|
+
this.events.push(event);
|
|
127
|
+
const payload = asRecord(event.payload);
|
|
128
|
+
switch (event.type) {
|
|
129
|
+
case "agent.message.delta": {
|
|
130
|
+
const text = stringValue(payload.text);
|
|
131
|
+
if (!text) return noStep();
|
|
132
|
+
const messageId = stringValue(payload.messageId) || null;
|
|
133
|
+
if (messageId) return this.messageDelta(messageId, text, isCommentary(payload));
|
|
134
|
+
const open = this.segments.at(-1);
|
|
135
|
+
if (isCommentary(payload)) {
|
|
136
|
+
if (open?.open && open.commentary) {
|
|
137
|
+
open.text += text;
|
|
138
|
+
} else {
|
|
139
|
+
this.closeSegments();
|
|
140
|
+
this.segments.push({ text, open: true, commentary: true, messageId: null });
|
|
141
|
+
}
|
|
142
|
+
return noStep();
|
|
143
|
+
}
|
|
144
|
+
if (open?.open && !open.commentary) {
|
|
145
|
+
open.text += text;
|
|
146
|
+
} else {
|
|
147
|
+
this.closeSegments();
|
|
148
|
+
return { chunks: [this.startSegment(text, true, null)], terminal: null };
|
|
149
|
+
}
|
|
150
|
+
return { chunks: [{ type: "text", text }], terminal: null };
|
|
151
|
+
}
|
|
152
|
+
case "agent.message.completed": {
|
|
153
|
+
const text = stringValue(payload.text) ?? "";
|
|
154
|
+
const messageId = stringValue(payload.messageId) || null;
|
|
155
|
+
if (messageId) return this.messageCompleted(messageId, text, isCommentary(payload));
|
|
156
|
+
const last = this.segments.at(-1);
|
|
157
|
+
if (isCommentary(payload)) {
|
|
158
|
+
if (last?.commentary && (last.open || text.startsWith(last.text))) {
|
|
159
|
+
last.open = false;
|
|
160
|
+
if (text) last.text = text;
|
|
161
|
+
return noStep();
|
|
162
|
+
}
|
|
163
|
+
// Undeclared deltas that the completion reveals as commentary were
|
|
164
|
+
// already streamed as reply text; keep the reply equal to the stream.
|
|
165
|
+
const streamed = last && !last.commentary && last.open && text.startsWith(last.text);
|
|
166
|
+
if (!streamed) {
|
|
167
|
+
if (text) {
|
|
168
|
+
this.closeSegments();
|
|
169
|
+
this.segments.push({ text, open: false, commentary: true, messageId: null });
|
|
170
|
+
}
|
|
171
|
+
return noStep();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const target = last?.commentary ? undefined : last;
|
|
175
|
+
if (!target || (!target.open && target.text && !text.startsWith(target.text))) {
|
|
176
|
+
if (!text) return noStep();
|
|
177
|
+
// The phase-less settlement copy of an answer this turn already
|
|
178
|
+
// completed message by message.
|
|
179
|
+
if (payload.phase === undefined && this.latestAnswer()?.text === text) return noStep();
|
|
180
|
+
this.closeSegments();
|
|
181
|
+
return { chunks: [this.startSegment(text, false, null)], terminal: null };
|
|
182
|
+
}
|
|
183
|
+
target.open = false;
|
|
184
|
+
return { chunks: this.extendAnswer(target, text), terminal: null };
|
|
185
|
+
}
|
|
186
|
+
case "agent.toolCall.created": {
|
|
187
|
+
this.closeSegments();
|
|
188
|
+
const name = stringValue(payload.name) ?? "tool";
|
|
189
|
+
const callId = stringValue(payload.id);
|
|
190
|
+
if (callId) this.openTools.set(callId, name);
|
|
191
|
+
return {
|
|
192
|
+
chunks: [
|
|
193
|
+
{
|
|
194
|
+
type: "tool",
|
|
195
|
+
name,
|
|
196
|
+
status: "started",
|
|
197
|
+
...(callId ? { callId } : {}),
|
|
198
|
+
...("arguments" in payload ? { input: payload.arguments } : {}),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
terminal: null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
case "agent.toolCall.output": {
|
|
205
|
+
const callId = stringValue(payload.id);
|
|
206
|
+
const name = (callId ? this.openTools.get(callId) : undefined) ?? "tool";
|
|
207
|
+
if (callId) this.openTools.delete(callId);
|
|
208
|
+
return {
|
|
209
|
+
chunks: [
|
|
210
|
+
{
|
|
211
|
+
type: "tool",
|
|
212
|
+
name,
|
|
213
|
+
status: isErrorOutput(payload) ? "failed" : "completed",
|
|
214
|
+
...(callId ? { callId } : {}),
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
terminal: null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "session.requiresAction": {
|
|
221
|
+
const pending = approvalPending(payload);
|
|
222
|
+
if (!pending) return { chunks: [], terminal: null };
|
|
223
|
+
this.pending = pending;
|
|
224
|
+
return {
|
|
225
|
+
chunks: [...this.settleSegments(), { type: "pending", pending }],
|
|
226
|
+
terminal: "pending",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
case "session.humanInput.requested": {
|
|
230
|
+
const pending = humanInputPending(payload);
|
|
231
|
+
if (!pending) return { chunks: [], terminal: null };
|
|
232
|
+
this.pending = pending;
|
|
233
|
+
return {
|
|
234
|
+
chunks: [...this.settleSegments(), { type: "pending", pending }],
|
|
235
|
+
terminal: "pending",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case "turn.completed":
|
|
239
|
+
this.emptyFinalReply = turnCompletedWithEmptyFinalReply(payload);
|
|
240
|
+
return { chunks: this.settleSegments(), terminal: "completed" };
|
|
241
|
+
case "turn.failed":
|
|
242
|
+
this.failure = event;
|
|
243
|
+
return { chunks: this.settleSegments(), terminal: "failed" };
|
|
244
|
+
case "turn.cancelled":
|
|
245
|
+
return { chunks: this.settleSegments(), terminal: "cancelled" };
|
|
246
|
+
default:
|
|
247
|
+
return { chunks: [], terminal: null };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
reply(terminal: ChatTurnTerminal): ChatReply {
|
|
252
|
+
const text = this.text;
|
|
253
|
+
return {
|
|
254
|
+
text,
|
|
255
|
+
sessionId: this.sessionId,
|
|
256
|
+
workspaceId: this.workspaceId,
|
|
257
|
+
turnId: this.turnId,
|
|
258
|
+
status:
|
|
259
|
+
terminal === "pending" ? "pending" : terminal === "cancelled" ? "cancelled" : "completed",
|
|
260
|
+
pending: this.pending,
|
|
261
|
+
...(this.emptyFinalReply
|
|
262
|
+
? { emptyFinalReply: true as const, notice: EMPTY_FINAL_REPLY_NOTICE }
|
|
263
|
+
: {}),
|
|
264
|
+
events: [...this.events],
|
|
265
|
+
toString: () => text,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The error to throw for a `turn.failed` settlement. */
|
|
270
|
+
failureError(): OpenGeniChatError {
|
|
271
|
+
const payload = asRecord(this.failure?.payload);
|
|
272
|
+
const code = stringValue(payload.code) ?? "turn_failed";
|
|
273
|
+
const message =
|
|
274
|
+
stringValue(payload.error) ?? stringValue(payload.message) ?? "The turn failed.";
|
|
275
|
+
return new OpenGeniChatError(code, message, this.failure);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private messageDelta(messageId: string, text: string, commentary: boolean): ChatFoldStep {
|
|
279
|
+
const segment = this.segmentOf(messageId);
|
|
280
|
+
if (segment?.open) {
|
|
281
|
+
segment.text += text;
|
|
282
|
+
return segment.commentary ? noStep() : { chunks: [{ type: "text", text }], terminal: null };
|
|
283
|
+
}
|
|
284
|
+
// Providers stream messages one after another: every earlier one is done.
|
|
285
|
+
this.closeSegments();
|
|
286
|
+
if (commentary) {
|
|
287
|
+
this.segments.push({ text, open: true, commentary: true, messageId });
|
|
288
|
+
return noStep();
|
|
289
|
+
}
|
|
290
|
+
return { chunks: [this.startSegment(text, true, messageId)], terminal: null };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private messageCompleted(messageId: string, text: string, commentary: boolean): ChatFoldStep {
|
|
294
|
+
const segment = this.segmentOf(messageId) ?? this.unidentifiedStreamingSegment(text);
|
|
295
|
+
if (segment) {
|
|
296
|
+
segment.messageId = messageId;
|
|
297
|
+
segment.open = false;
|
|
298
|
+
if (segment.commentary) {
|
|
299
|
+
if (text) segment.text = text;
|
|
300
|
+
return noStep();
|
|
301
|
+
}
|
|
302
|
+
// Streamed as reply text, even when the completion reveals commentary:
|
|
303
|
+
// the reply stays equal to what append-only consumers already have.
|
|
304
|
+
return { chunks: this.extendAnswer(segment, text), terminal: null };
|
|
305
|
+
}
|
|
306
|
+
if (!text) return noStep();
|
|
307
|
+
if (commentary) {
|
|
308
|
+
this.segments.push({ text, open: false, commentary: true, messageId });
|
|
309
|
+
return noStep();
|
|
310
|
+
}
|
|
311
|
+
return { chunks: [this.startSegment(text, false, messageId)], terminal: null };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private segmentOf(messageId: string): Segment | undefined {
|
|
315
|
+
return this.findLastSegment((segment) => segment.messageId === messageId);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Deltas that carried no identity, still streaming the text this completion names. */
|
|
319
|
+
private unidentifiedStreamingSegment(text: string): Segment | undefined {
|
|
320
|
+
const last = this.segments.at(-1);
|
|
321
|
+
return last?.open && last.messageId === null && text.startsWith(last.text) ? last : undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private latestAnswer(): Segment | undefined {
|
|
325
|
+
return this.findLastSegment((segment) => !segment.commentary);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private findLastSegment(predicate: (segment: Segment) => boolean): Segment | undefined {
|
|
329
|
+
for (let index = this.segments.length - 1; index >= 0; index -= 1) {
|
|
330
|
+
const segment = this.segments[index]!;
|
|
331
|
+
if (predicate(segment)) return segment;
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Complete an answer segment. Only the newest answer can grow: a remainder
|
|
338
|
+
* for an earlier one would land after later text for append-only consumers.
|
|
339
|
+
*/
|
|
340
|
+
private extendAnswer(segment: Segment, text: string): ChatChunk[] {
|
|
341
|
+
if (text.length <= segment.text.length || !text.startsWith(segment.text)) return [];
|
|
342
|
+
if (segment !== this.latestAnswer()) return [];
|
|
343
|
+
const remainder = text.slice(segment.text.length);
|
|
344
|
+
segment.text = text;
|
|
345
|
+
return [{ type: "text", text: remainder }];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private closeSegments(): void {
|
|
349
|
+
for (const segment of this.segments) segment.open = false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Close the turn's text. A turn that produced commentary but no answer text
|
|
354
|
+
* (for example one that ends waiting for a worker) replies with its latest
|
|
355
|
+
* commentary rather than nothing.
|
|
356
|
+
*/
|
|
357
|
+
private settleSegments(): ChatChunk[] {
|
|
358
|
+
this.closeSegments();
|
|
359
|
+
// A typed missing-final notice must not turn progress commentary into
|
|
360
|
+
// the missing answer. Ordinary waits retain their historical fallback.
|
|
361
|
+
if (this.emptyFinalReply) return [];
|
|
362
|
+
if (this.segments.some((segment) => !segment.commentary)) return [];
|
|
363
|
+
const latest = [...this.segments]
|
|
364
|
+
.reverse()
|
|
365
|
+
.find((segment) => segment.commentary && segment.text);
|
|
366
|
+
if (!latest) return [];
|
|
367
|
+
latest.commentary = false;
|
|
368
|
+
return [{ type: "text", text: latest.text }];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private startSegment(text: string, open: boolean, messageId: string | null): ChatChunk {
|
|
372
|
+
// Match the separator used by `text` before exposing the next segment to
|
|
373
|
+
// append-only consumers (React and both protocol adapters).
|
|
374
|
+
const separator = this.segments.some((segment) => !segment.commentary) ? "\n\n" : "";
|
|
375
|
+
this.segments.push({ text, open, commentary: false, messageId });
|
|
376
|
+
return { type: "text", text: separator + text };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isCommentary(payload: Record<string, unknown>): boolean {
|
|
381
|
+
return payload.phase === "commentary";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isTurnScopedType(type: string): boolean {
|
|
385
|
+
return type.startsWith("turn.") || type.startsWith("agent.") || type === "user.message";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function approvalPending(payload: Record<string, unknown>): ChatPending | null {
|
|
389
|
+
const approvals = Array.isArray(payload.approvals) ? payload.approvals : [];
|
|
390
|
+
const first = approvals[0];
|
|
391
|
+
if (!first) return null;
|
|
392
|
+
const raw = asRecord(first);
|
|
393
|
+
const rawItem = asRecord(raw.rawItem);
|
|
394
|
+
const requestId =
|
|
395
|
+
stringValue(rawItem.callId) ??
|
|
396
|
+
stringValue(rawItem.id) ??
|
|
397
|
+
stringValue(raw.id) ??
|
|
398
|
+
stringValue(raw.callId);
|
|
399
|
+
if (!requestId) return null;
|
|
400
|
+
return {
|
|
401
|
+
kind: "approval",
|
|
402
|
+
requestId,
|
|
403
|
+
name: stringValue(raw.name) ?? stringValue(raw.toolName) ?? stringValue(rawItem.name) ?? null,
|
|
404
|
+
payload: first,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function humanInputPending(payload: Record<string, unknown>): ChatPending | null {
|
|
409
|
+
const request = asRecord(payload.request);
|
|
410
|
+
const requestId = stringValue(request.id);
|
|
411
|
+
if (!requestId || !Array.isArray(request.questions)) return null;
|
|
412
|
+
return { kind: "human_input", requestId, name: null, payload: payload.request };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function asRecord(value: unknown): Record<string, unknown> {
|
|
416
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
417
|
+
? (value as Record<string, unknown>)
|
|
418
|
+
: {};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function stringValue(value: unknown): string | undefined {
|
|
422
|
+
return typeof value === "string" ? value : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isErrorOutput(payload: Record<string, unknown>): boolean {
|
|
426
|
+
if (payload.error === true || payload.failed === true) return true;
|
|
427
|
+
const output = payload.output;
|
|
428
|
+
return (
|
|
429
|
+
!!output && typeof output === "object" && (output as { isError?: unknown }).isError === true
|
|
430
|
+
);
|
|
431
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { parseSseStream } from "../sse";
|
|
2
|
+
import type { HumanInputAnswer } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
chatErrorSummary,
|
|
5
|
+
clientConversation,
|
|
6
|
+
errorResponse,
|
|
7
|
+
jsonResponse,
|
|
8
|
+
openResolvedChat,
|
|
9
|
+
readJsonObject,
|
|
10
|
+
resolveChatRequest,
|
|
11
|
+
sseByteStream,
|
|
12
|
+
sseHeaders,
|
|
13
|
+
sseLine,
|
|
14
|
+
type ChatResolve,
|
|
15
|
+
} from "./http";
|
|
16
|
+
import { handleChatCompletionsRequest, handleResponsesRequest } from "./openai";
|
|
17
|
+
import type { Opengeni } from "./opengeni";
|
|
18
|
+
import { OpenGeniChatError, type ChatChunk, type ChatRespondInput } from "./types";
|
|
19
|
+
import { handleVercelChatRequest } from "./vercel";
|
|
20
|
+
|
|
21
|
+
export { CHAT_CONVERSATION_HEADER, type ChatResolution, type ChatResolve } from "./http";
|
|
22
|
+
|
|
23
|
+
export type ChatHandlerFormat = "native" | "vercel" | "openai-chat" | "openai-responses";
|
|
24
|
+
|
|
25
|
+
export type ChatHandlerOptions = {
|
|
26
|
+
/** Mandatory host auth hook; see {@link ChatResolve}. */
|
|
27
|
+
resolve: ChatResolve;
|
|
28
|
+
/** Default wire format; a request may override it with the format header. */
|
|
29
|
+
format?: ChatHandlerFormat | undefined;
|
|
30
|
+
/** Vercel format only: also emit Opengeni's own tool activity (see `UIMessageStreamOptions.toolParts`). */
|
|
31
|
+
toolParts?: boolean | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Per-request wire-format override header. */
|
|
35
|
+
export const CHAT_FORMAT_HEADER = "x-opengeni-chat-format";
|
|
36
|
+
|
|
37
|
+
const CHAT_FORMATS: ReadonlySet<string> = new Set([
|
|
38
|
+
"native",
|
|
39
|
+
"vercel",
|
|
40
|
+
"openai-chat",
|
|
41
|
+
"openai-responses",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function methodNotAllowed(allow: string): Response {
|
|
45
|
+
return new Response(
|
|
46
|
+
JSON.stringify({ error: { message: `Use ${allow}.`, code: "method_not_allowed" } }),
|
|
47
|
+
{ status: 405, headers: { Allow: allow, "Content-Type": "application/json; charset=utf-8" } },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One request handler for a product's chat endpoint. `POST` with `{ message }`
|
|
53
|
+
* streams the reply in the selected format; `POST .../respond` answers a
|
|
54
|
+
* pending approval or human-input request and streams the continuation;
|
|
55
|
+
* `GET` returns the conversation's history as JSON. Every other method is a
|
|
56
|
+
* 405. The conversation is the host's resolution, else the
|
|
57
|
+
* `x-opengeni-conversation` header, else the wire format's own field.
|
|
58
|
+
*/
|
|
59
|
+
export function createChatHandler(
|
|
60
|
+
og: Opengeni,
|
|
61
|
+
options: ChatHandlerOptions,
|
|
62
|
+
): (request: Request) => Promise<Response> {
|
|
63
|
+
return async (request) => {
|
|
64
|
+
const isRespond = new URL(request.url).pathname.replace(/\/+$/, "").endsWith("/respond");
|
|
65
|
+
if (isRespond) {
|
|
66
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
67
|
+
return await handleNativeRespondRequest(og, request, options.resolve);
|
|
68
|
+
}
|
|
69
|
+
if (request.method === "GET") {
|
|
70
|
+
return await handleNativeHistoryRequest(og, request, options.resolve);
|
|
71
|
+
}
|
|
72
|
+
if (request.method !== "POST") return methodNotAllowed("GET, POST");
|
|
73
|
+
const headerFormat = request.headers.get(CHAT_FORMAT_HEADER);
|
|
74
|
+
if (headerFormat !== null && !CHAT_FORMATS.has(headerFormat)) {
|
|
75
|
+
return errorResponse(400, `Unknown chat format: ${headerFormat}`, "unknown_format");
|
|
76
|
+
}
|
|
77
|
+
const format = (headerFormat as ChatHandlerFormat | null) ?? options.format ?? "native";
|
|
78
|
+
switch (format) {
|
|
79
|
+
case "vercel":
|
|
80
|
+
return await handleVercelChatRequest(og, request, options.resolve, {
|
|
81
|
+
toolParts: options.toolParts,
|
|
82
|
+
});
|
|
83
|
+
case "openai-chat":
|
|
84
|
+
return await handleChatCompletionsRequest(og, request, options.resolve);
|
|
85
|
+
case "openai-responses":
|
|
86
|
+
return await handleResponsesRequest(og, request, options.resolve);
|
|
87
|
+
default:
|
|
88
|
+
return await handleNativeChatRequest(og, request, options.resolve);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `POST { message }` -> native SSE of {@link ChatChunk} (`event: chunk`). */
|
|
94
|
+
export async function handleNativeChatRequest(
|
|
95
|
+
og: Opengeni,
|
|
96
|
+
request: Request,
|
|
97
|
+
resolve: ChatResolve,
|
|
98
|
+
): Promise<Response> {
|
|
99
|
+
const body = await readJsonObject(request);
|
|
100
|
+
const message = typeof body?.message === "string" ? body.message.trim() : "";
|
|
101
|
+
if (!message) {
|
|
102
|
+
return errorResponse(400, "Body must carry a non-empty message.", "message_required");
|
|
103
|
+
}
|
|
104
|
+
const resolved = await resolveChatRequest(request, resolve);
|
|
105
|
+
if (resolved.response) return resolved.response;
|
|
106
|
+
const opened = await openResolvedChat(og, resolved.resolution, clientConversation(request, null));
|
|
107
|
+
if (opened.response) return opened.response;
|
|
108
|
+
return chatChunksToSseResponse(opened.chat.stream(message, { signal: request.signal }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `GET` -> `{ conversation, sessionId, created, messages, pending, status }`: the user and
|
|
113
|
+
* assistant text of the conversation so a client can restore it on reload
|
|
114
|
+
* (`messages` is empty and `created` false before the first message).
|
|
115
|
+
*/
|
|
116
|
+
export async function handleNativeHistoryRequest(
|
|
117
|
+
og: Opengeni,
|
|
118
|
+
request: Request,
|
|
119
|
+
resolve: ChatResolve,
|
|
120
|
+
): Promise<Response> {
|
|
121
|
+
const resolved = await resolveChatRequest(request, resolve);
|
|
122
|
+
if (resolved.response) return resolved.response;
|
|
123
|
+
const opened = await openResolvedChat(og, resolved.resolution, clientConversation(request, null));
|
|
124
|
+
if (opened.response) return opened.response;
|
|
125
|
+
try {
|
|
126
|
+
const snapshot = await opened.chat.snapshot();
|
|
127
|
+
return jsonResponse({
|
|
128
|
+
conversation: opened.chat.conversation,
|
|
129
|
+
sessionId: opened.chat.sessionId,
|
|
130
|
+
created: opened.chat.created,
|
|
131
|
+
...snapshot,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const summary = chatErrorSummary(error);
|
|
135
|
+
return errorResponse(summary.status, summary.message, summary.code);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `POST .../respond { requestId, decision | answers | skip }` -> native SSE of the continuation. */
|
|
140
|
+
export async function handleNativeRespondRequest(
|
|
141
|
+
og: Opengeni,
|
|
142
|
+
request: Request,
|
|
143
|
+
resolve: ChatResolve,
|
|
144
|
+
): Promise<Response> {
|
|
145
|
+
const body = await readJsonObject(request);
|
|
146
|
+
const input = respondInputFromBody(body);
|
|
147
|
+
if (!input) {
|
|
148
|
+
return errorResponse(
|
|
149
|
+
400,
|
|
150
|
+
"Body must carry requestId plus decision, answers, or skip.",
|
|
151
|
+
"respond_input_invalid",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const resolved = await resolveChatRequest(request, resolve);
|
|
155
|
+
if (resolved.response) return resolved.response;
|
|
156
|
+
const opened = await openResolvedChat(og, resolved.resolution, clientConversation(request, null));
|
|
157
|
+
if (opened.response) return opened.response;
|
|
158
|
+
return chatChunksToSseResponse(opened.chat.respondStream(input, { signal: request.signal }));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function respondInputFromBody(
|
|
162
|
+
body: Record<string, unknown> | null,
|
|
163
|
+
): ChatRespondInput | null {
|
|
164
|
+
const requestId = typeof body?.requestId === "string" ? body.requestId : "";
|
|
165
|
+
if (!body || !requestId) return null;
|
|
166
|
+
if (body.decision === "approve" || body.decision === "reject") {
|
|
167
|
+
return {
|
|
168
|
+
requestId,
|
|
169
|
+
decision: body.decision,
|
|
170
|
+
...(typeof body.message === "string" ? { message: body.message } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(body.answers)) {
|
|
174
|
+
const answers = body.answers.filter(
|
|
175
|
+
(answer): answer is HumanInputAnswer =>
|
|
176
|
+
!!answer &&
|
|
177
|
+
typeof answer === "object" &&
|
|
178
|
+
typeof (answer as HumanInputAnswer).questionId === "string" &&
|
|
179
|
+
Array.isArray((answer as HumanInputAnswer).values),
|
|
180
|
+
);
|
|
181
|
+
return { requestId, answers };
|
|
182
|
+
}
|
|
183
|
+
if (body.skip === true) return { requestId, skip: true };
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Native wire format: `event: chunk` per {@link ChatChunk}, `event: error` on failure. */
|
|
188
|
+
export async function* chatChunksToSseBlocks(
|
|
189
|
+
chunks: AsyncIterable<ChatChunk>,
|
|
190
|
+
): AsyncGenerator<string, void, void> {
|
|
191
|
+
try {
|
|
192
|
+
for await (const chunk of chunks) {
|
|
193
|
+
yield sseLine(JSON.stringify(chunk), "chunk");
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const summary = chatErrorSummary(error);
|
|
197
|
+
yield sseLine(JSON.stringify({ code: summary.code, message: summary.message }), "error");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function chatChunksToSseStream(
|
|
202
|
+
chunks: AsyncIterable<ChatChunk>,
|
|
203
|
+
onCancel?: () => void,
|
|
204
|
+
): ReadableStream<Uint8Array> {
|
|
205
|
+
return sseByteStream(chatChunksToSseBlocks(chunks), onCancel);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function chatChunksToSseResponse(chunks: AsyncIterable<ChatChunk>): Response {
|
|
209
|
+
return new Response(chatChunksToSseStream(chunks), {
|
|
210
|
+
headers: sseHeaders({ [CHAT_FORMAT_HEADER]: "native" }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Browser-side reader for the native wire format. Throws {@link OpenGeniChatError} on `event: error`. */
|
|
215
|
+
export async function* parseChatChunkStream(
|
|
216
|
+
stream: ReadableStream<Uint8Array>,
|
|
217
|
+
): AsyncGenerator<ChatChunk, void, void> {
|
|
218
|
+
for await (const message of parseSseStream(stream)) {
|
|
219
|
+
let parsed: unknown;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(message.data);
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (message.event === "error") {
|
|
226
|
+
const record = (parsed ?? {}) as { code?: unknown; message?: unknown };
|
|
227
|
+
throw new OpenGeniChatError(
|
|
228
|
+
typeof record.code === "string" ? record.code : "chat_error",
|
|
229
|
+
typeof record.message === "string" ? record.message : "Chat request failed.",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
parsed &&
|
|
234
|
+
typeof parsed === "object" &&
|
|
235
|
+
typeof (parsed as { type?: unknown }).type === "string"
|
|
236
|
+
) {
|
|
237
|
+
yield parsed as ChatChunk;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|