chatkit-bun 0.0.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 +202 -0
- package/package.json +40 -0
- package/src/actions.ts +39 -0
- package/src/agents/accumulate.ts +43 -0
- package/src/agents/annotations.ts +157 -0
- package/src/agents/context.ts +190 -0
- package/src/agents/converter.ts +290 -0
- package/src/agents/index.ts +25 -0
- package/src/agents/stream.ts +1053 -0
- package/src/agents/types.ts +30 -0
- package/src/agents/workflows.ts +220 -0
- package/src/errors.ts +19 -0
- package/src/http.ts +60 -0
- package/src/index.ts +11 -0
- package/src/serialization.ts +75 -0
- package/src/server.ts +874 -0
- package/src/sqlite-store.ts +400 -0
- package/src/store.ts +98 -0
- package/src/types/core.ts +322 -0
- package/src/types/server.ts +396 -0
- package/src/widgets/components.ts +188 -0
- package/src/widgets/diff.ts +151 -0
- package/src/widgets/index.ts +6 -0
- package/src/widgets/serialization.ts +46 -0
- package/src/widgets/stream.ts +104 -0
- package/src/widgets/template.ts +180 -0
- package/src/widgets/types.ts +52 -0
- package/types/actions.d.ts +19 -0
- package/types/agents/accumulate.d.ts +4 -0
- package/types/agents/annotations.d.ts +21 -0
- package/types/agents/context.d.ts +35 -0
- package/types/agents/converter.d.ts +60 -0
- package/types/agents/index.d.ts +9 -0
- package/types/agents/stream.d.ts +4 -0
- package/types/agents/types.d.ts +26 -0
- package/types/agents/workflows.d.ts +34 -0
- package/types/errors.d.ts +11 -0
- package/types/http.d.ts +6 -0
- package/types/index.d.ts +11 -0
- package/types/serialization.d.ts +8 -0
- package/types/server.d.ts +73 -0
- package/types/sqlite-store.d.ts +43 -0
- package/types/store.d.ts +45 -0
- package/types/types/core.d.ts +1220 -0
- package/types/types/server.d.ts +5841 -0
- package/types/widgets/components.d.ts +144 -0
- package/types/widgets/diff.d.ts +7 -0
- package/types/widgets/index.d.ts +6 -0
- package/types/widgets/serialization.d.ts +2 -0
- package/types/widgets/stream.d.ts +10 -0
- package/types/widgets/template.d.ts +19 -0
- package/types/widgets/types.d.ts +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# chatkit-bun
|
|
2
|
+
|
|
3
|
+
`chatkit-bun` is a Bun-native server bridge for ChatKit-style thread APIs. It includes:
|
|
4
|
+
|
|
5
|
+
- ChatKit request processing and SSE response helpers.
|
|
6
|
+
- SQLite-backed thread and item storage.
|
|
7
|
+
- Widget serialization and streaming helpers.
|
|
8
|
+
- `@openai/agents` stream conversion helpers for Bun servers.
|
|
9
|
+
|
|
10
|
+
## Development
|
|
11
|
+
|
|
12
|
+
Install dependencies:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run typecheck and tests:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun run verify
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The package is source-distributed for Bun apps. Its package entrypoint is `src/index.ts`, so a private Git install does not need a build step or committed `dist` output.
|
|
25
|
+
|
|
26
|
+
Install from a private GitHub URL in a Bun app:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@openai/agents": "^0.11.6",
|
|
32
|
+
"chatkit-bun": "git+ssh://git@github.com/ottersoft-x/chatkit-bun.git#main"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Bun Agent Server Example
|
|
38
|
+
|
|
39
|
+
Use `ChatKitServer` to bridge ChatKit requests to an `@openai/agents` workflow. This example streams an intake agent first, passes its summary to an isolated research agent that does not receive the prior chat history, then passes both outputs to the final answer agent. Each stage emits workflow updates so the frontend can show what is happening:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Agent, run } from "@openai/agents";
|
|
43
|
+
import {
|
|
44
|
+
AgentContext,
|
|
45
|
+
ChatKitServer,
|
|
46
|
+
SQLiteStore,
|
|
47
|
+
createChatKitHandler,
|
|
48
|
+
simpleToAgentInput,
|
|
49
|
+
streamAgentResponse,
|
|
50
|
+
type ThreadItem,
|
|
51
|
+
type ThreadMetadata,
|
|
52
|
+
type ThreadStreamEvent,
|
|
53
|
+
} from "chatkit-bun";
|
|
54
|
+
|
|
55
|
+
interface RequestContext {
|
|
56
|
+
userId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type UserMessageItem = Extract<ThreadItem, { type: "user_message" }>;
|
|
60
|
+
|
|
61
|
+
const intakeAgent = new Agent({
|
|
62
|
+
name: "Intake Agent",
|
|
63
|
+
instructions:
|
|
64
|
+
"Read the conversation and summarize the user's goal, constraints, and any missing context.",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const answerAgent = new Agent({
|
|
68
|
+
name: "Answer Agent",
|
|
69
|
+
instructions:
|
|
70
|
+
"Use the intake summary and research notes to produce a concise, helpful final answer for the user.",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const researchAgent = new Agent({
|
|
74
|
+
name: "Research Agent",
|
|
75
|
+
instructions:
|
|
76
|
+
"You receive only a task summary, not the conversation history. Return focused research notes.",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function requestContext(request: Request): RequestContext {
|
|
80
|
+
return {
|
|
81
|
+
userId: request.headers.get("x-user-id") ?? "anonymous",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function threadPreviousResponseId(thread: ThreadMetadata): string | null {
|
|
86
|
+
const value = thread.metadata.previous_response_id;
|
|
87
|
+
return typeof value === "string" ? value : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class AppChatKitServer extends ChatKitServer<RequestContext> {
|
|
91
|
+
constructor(readonly sqlitePath = Bun.env.CHATKIT_SQLITE_PATH ?? "chatkit.sqlite") {
|
|
92
|
+
super(
|
|
93
|
+
new SQLiteStore<RequestContext>({
|
|
94
|
+
path: sqlitePath,
|
|
95
|
+
getUserId: (context) => context.userId,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override async *respond(
|
|
101
|
+
thread: ThreadMetadata,
|
|
102
|
+
_inputUserMessage: UserMessageItem | null,
|
|
103
|
+
context: RequestContext,
|
|
104
|
+
): AsyncIterable<ThreadStreamEvent> {
|
|
105
|
+
const page = await this.store.loadThreadItems(thread.id, null, 50, "asc", context);
|
|
106
|
+
const input = await simpleToAgentInput(page.data);
|
|
107
|
+
const previousResponseId = threadPreviousResponseId(thread);
|
|
108
|
+
|
|
109
|
+
const intakeContext = new AgentContext({
|
|
110
|
+
thread,
|
|
111
|
+
store: this.store,
|
|
112
|
+
context,
|
|
113
|
+
previousResponseId,
|
|
114
|
+
});
|
|
115
|
+
intakeContext.addWorkflowTask({
|
|
116
|
+
type: "custom",
|
|
117
|
+
title: "Reviewing the request",
|
|
118
|
+
content: "The intake agent is identifying the user's goal and constraints.",
|
|
119
|
+
status_indicator: "loading",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const intakeRun = await run(intakeAgent, input, {
|
|
123
|
+
stream: true,
|
|
124
|
+
previousResponseId: previousResponseId ?? undefined,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
yield* streamAgentResponse(intakeContext, intakeRun);
|
|
128
|
+
await intakeRun.completed;
|
|
129
|
+
|
|
130
|
+
const intakeSummary = String(intakeRun.finalOutput ?? "No intake summary was produced.");
|
|
131
|
+
const researchContext = new AgentContext({
|
|
132
|
+
thread,
|
|
133
|
+
store: this.store,
|
|
134
|
+
context,
|
|
135
|
+
});
|
|
136
|
+
researchContext.addWorkflowTask({
|
|
137
|
+
type: "custom",
|
|
138
|
+
title: "Checking isolated context",
|
|
139
|
+
content: "The research agent is working from the intake summary only.",
|
|
140
|
+
status_indicator: "loading",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const researchRun = await run(
|
|
144
|
+
researchAgent,
|
|
145
|
+
`Research this request using only this summary:\n\n${intakeSummary}`,
|
|
146
|
+
{ stream: true },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
yield* streamAgentResponse(researchContext, researchRun);
|
|
150
|
+
await researchRun.completed;
|
|
151
|
+
|
|
152
|
+
const researchNotes = String(researchRun.finalOutput ?? "No research notes were produced.");
|
|
153
|
+
const answerContext = new AgentContext({
|
|
154
|
+
thread,
|
|
155
|
+
store: this.store,
|
|
156
|
+
context,
|
|
157
|
+
previousResponseId: intakeRun.lastResponseId ?? previousResponseId,
|
|
158
|
+
});
|
|
159
|
+
answerContext.addWorkflowTask({
|
|
160
|
+
type: "custom",
|
|
161
|
+
title: "Drafting the answer",
|
|
162
|
+
content: "The answer agent is combining the intake summary and isolated research notes.",
|
|
163
|
+
status_indicator: "loading",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const answerRun = await run(
|
|
167
|
+
answerAgent,
|
|
168
|
+
`Use this intake summary and research notes.
|
|
169
|
+
|
|
170
|
+
Intake summary:
|
|
171
|
+
${intakeSummary}
|
|
172
|
+
|
|
173
|
+
Research notes:
|
|
174
|
+
${researchNotes}`,
|
|
175
|
+
{
|
|
176
|
+
stream: true,
|
|
177
|
+
previousResponseId: answerContext.previousResponseId ?? undefined,
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
yield* streamAgentResponse(answerContext, answerRun);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const chatkitHandler = createChatKitHandler(new AppChatKitServer(), {
|
|
186
|
+
getContext: requestContext,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const server = Bun.serve({
|
|
190
|
+
port: Number(Bun.env.PORT ?? 3000),
|
|
191
|
+
routes: {
|
|
192
|
+
"/health": new Response("ok"),
|
|
193
|
+
"/chatkit": {
|
|
194
|
+
POST: chatkitHandler,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
console.log(`ChatKit server listening on ${server.url}`);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The server listens on `PORT` or `3000` and exposes `POST /chatkit`. It uses `x-user-id` as the per-request user id, falling back to `anonymous`.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chatkit-bun",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"module": "./src/index.ts",
|
|
5
|
+
"types": "./types/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"types"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./types/index.d.ts",
|
|
17
|
+
"import": "./src/index.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "^1.3.14",
|
|
22
|
+
"@types/nunjucks": "^3.2.6",
|
|
23
|
+
"typescript": "^6.0.3"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"typescript": "^6"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@openai/agents": "^0.11.5",
|
|
30
|
+
"nunjucks": "^3.2.4",
|
|
31
|
+
"zod": "^4.4.3"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"build:types": "bunx tsc -p tsconfig.types.json",
|
|
36
|
+
"typecheck": "bunx tsc --noEmit",
|
|
37
|
+
"verify": "bun run typecheck && bun test",
|
|
38
|
+
"verify:parity": "bun run verify && bun scripts/verify-parity.ts"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const HandlerSchema = z.union([z.literal("client"), z.literal("server")]);
|
|
4
|
+
export type Handler = z.infer<typeof HandlerSchema>;
|
|
5
|
+
|
|
6
|
+
export const LoadingBehaviorSchema = z.union([
|
|
7
|
+
z.literal("auto"),
|
|
8
|
+
z.literal("none"),
|
|
9
|
+
z.literal("self"),
|
|
10
|
+
z.literal("container"),
|
|
11
|
+
]);
|
|
12
|
+
export type LoadingBehavior = z.infer<typeof LoadingBehaviorSchema>;
|
|
13
|
+
|
|
14
|
+
export const ActionConfigSchema = z.object({
|
|
15
|
+
type: z.string(),
|
|
16
|
+
payload: z.unknown().optional(),
|
|
17
|
+
handler: HandlerSchema.default("server"),
|
|
18
|
+
loadingBehavior: LoadingBehaviorSchema.default("auto"),
|
|
19
|
+
streaming: z.boolean().default(true),
|
|
20
|
+
});
|
|
21
|
+
export type ActionConfig = z.infer<typeof ActionConfigSchema>;
|
|
22
|
+
|
|
23
|
+
export interface CreateActionOptions {
|
|
24
|
+
handler?: Handler;
|
|
25
|
+
loadingBehavior?: LoadingBehavior;
|
|
26
|
+
streaming?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createActionConfig(
|
|
30
|
+
type: string,
|
|
31
|
+
payload?: unknown,
|
|
32
|
+
options: CreateActionOptions = {},
|
|
33
|
+
): ActionConfig {
|
|
34
|
+
return ActionConfigSchema.parse({
|
|
35
|
+
type,
|
|
36
|
+
payload,
|
|
37
|
+
...options,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MarkdownWidget, TextWidget } from "../widgets";
|
|
2
|
+
|
|
3
|
+
type AccumulatableTextWidget = TextWidget | MarkdownWidget;
|
|
4
|
+
type UnknownRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
7
|
+
return typeof value === "object" && value !== null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function outputTextDelta(event: unknown): string | null {
|
|
11
|
+
if (!isRecord(event) || event.type !== "raw_response_event" || !isRecord(event.data)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (event.data.type !== "response.output_text.delta") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return typeof event.data.delta === "string" ? event.data.delta : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type { AccumulatableTextWidget };
|
|
23
|
+
|
|
24
|
+
export async function* accumulateText<TWidget extends AccumulatableTextWidget>(
|
|
25
|
+
events: AsyncIterable<unknown>,
|
|
26
|
+
baseWidget: TWidget,
|
|
27
|
+
): AsyncGenerator<TWidget, void, unknown> {
|
|
28
|
+
let text = "";
|
|
29
|
+
|
|
30
|
+
yield baseWidget;
|
|
31
|
+
|
|
32
|
+
for await (const event of events) {
|
|
33
|
+
const delta = outputTextDelta(event);
|
|
34
|
+
if (delta === null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
text += delta;
|
|
39
|
+
yield { ...baseWidget, value: text } as TWidget;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
yield { ...baseWidget, value: text, streaming: false } as TWidget;
|
|
43
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Annotation } from "../types/core";
|
|
2
|
+
|
|
3
|
+
type UnknownRecord = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stringValue(value: unknown): string | null {
|
|
10
|
+
return typeof value === "string" ? value : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nonEmptyStringValue(value: unknown): string | null {
|
|
14
|
+
const text = stringValue(value);
|
|
15
|
+
return text && text.length > 0 ? text : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function numberValue(value: unknown): number | null {
|
|
19
|
+
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResponseStreamConverterOptions {
|
|
23
|
+
partialImages?: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ResponseStreamConverter {
|
|
27
|
+
private readonly partialImages: number | null;
|
|
28
|
+
|
|
29
|
+
constructor(options: ResponseStreamConverterOptions = {}) {
|
|
30
|
+
this.partialImages = options.partialImages ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
base64ImageToUrl(
|
|
34
|
+
_imageId: string,
|
|
35
|
+
base64Image: string,
|
|
36
|
+
_partialImageIndex: number | null = null,
|
|
37
|
+
): string | Promise<string> {
|
|
38
|
+
return Promise.resolve(`data:image/png;base64,${base64Image}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
partialImageIndexToProgress(partialImageIndex: number): number {
|
|
42
|
+
if (this.partialImages === null || this.partialImages <= 0) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Math.min(1, partialImageIndex / this.partialImages);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
convertAnnotation(annotation: unknown): Annotation | null {
|
|
50
|
+
if (!isRecord(annotation)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
switch (annotation.type) {
|
|
55
|
+
case "file_citation":
|
|
56
|
+
return this.fileCitationToAnnotation(annotation);
|
|
57
|
+
case "container_file_citation":
|
|
58
|
+
return this.containerFileCitationToAnnotation(annotation);
|
|
59
|
+
case "url_citation":
|
|
60
|
+
return this.urlCitationToAnnotation(annotation);
|
|
61
|
+
default:
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fileCitationToAnnotation(annotation: unknown): Annotation | null {
|
|
67
|
+
if (!isRecord(annotation)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const filename = nonEmptyStringValue(annotation.filename);
|
|
72
|
+
if (!filename) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
type: "annotation",
|
|
78
|
+
source: { type: "file", filename, title: filename },
|
|
79
|
+
index: numberValue(annotation.index),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
containerFileCitationToAnnotation(annotation: unknown): Annotation | null {
|
|
84
|
+
if (!isRecord(annotation)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const filename = nonEmptyStringValue(annotation.filename);
|
|
89
|
+
if (!filename) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
type: "annotation",
|
|
95
|
+
source: { type: "file", filename, title: filename },
|
|
96
|
+
index: numberValue(annotation.end_index),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
urlCitationToAnnotation(annotation: unknown): Annotation | null {
|
|
101
|
+
if (!isRecord(annotation)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const url = nonEmptyStringValue(annotation.url);
|
|
106
|
+
const title = stringValue(annotation.title);
|
|
107
|
+
if (!url || title === null) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
type: "annotation",
|
|
113
|
+
source: { type: "url", url, title },
|
|
114
|
+
index: numberValue(annotation.end_index),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ConvertedTextContent {
|
|
120
|
+
type: "output_text";
|
|
121
|
+
text: string;
|
|
122
|
+
annotations: Annotation[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function convertTextContentPart(
|
|
126
|
+
part: unknown,
|
|
127
|
+
converter: ResponseStreamConverter,
|
|
128
|
+
): ConvertedTextContent | null {
|
|
129
|
+
if (!isRecord(part)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (part.type === "refusal") {
|
|
134
|
+
const text = stringValue(part.refusal);
|
|
135
|
+
return text === null ? null : { type: "output_text", text, annotations: [] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (part.type !== "output_text") {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const text = stringValue(part.text);
|
|
143
|
+
if (text === null) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const annotations = Array.isArray(part.annotations)
|
|
148
|
+
? part.annotations.flatMap((annotation) => {
|
|
149
|
+
const converted = converter.convertAnnotation(annotation);
|
|
150
|
+
return converted ? [converted] : [];
|
|
151
|
+
})
|
|
152
|
+
: [];
|
|
153
|
+
|
|
154
|
+
return { type: "output_text", text, annotations };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const defaultResponseStreamConverter = new ResponseStreamConverter();
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Task, ThreadItem, Workflow, WorkflowSummary } from "../types/core";
|
|
2
|
+
import { ThreadStreamEventSchema, type ThreadStreamEvent } from "../types/server";
|
|
3
|
+
import { streamWidget as streamWidgetEvents, type WidgetRoot } from "../widgets";
|
|
4
|
+
import type { AgentContextOptions, JsonObject } from "./types";
|
|
5
|
+
import {
|
|
6
|
+
appendWorkflowTask,
|
|
7
|
+
createWorkflowItem,
|
|
8
|
+
finishWorkflow,
|
|
9
|
+
normalizeWorkflowTask,
|
|
10
|
+
shouldEmitWorkflowAdded,
|
|
11
|
+
updateWorkflowTaskEvent,
|
|
12
|
+
workflowAddedEvent,
|
|
13
|
+
} from "./workflows";
|
|
14
|
+
|
|
15
|
+
class AsyncEventQueue<T> implements AsyncIterable<T> {
|
|
16
|
+
private readonly values: T[] = [];
|
|
17
|
+
private readonly waiting: Array<(result: IteratorResult<T>) => void> = [];
|
|
18
|
+
private closed = false;
|
|
19
|
+
|
|
20
|
+
push(value: T): void {
|
|
21
|
+
if (this.closed) {
|
|
22
|
+
throw new Error("Cannot stream events after the agent context has completed.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const resolve = this.waiting.shift();
|
|
26
|
+
|
|
27
|
+
if (resolve) {
|
|
28
|
+
resolve({ done: false, value });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.values.push(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
close(): void {
|
|
36
|
+
if (this.closed) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.closed = true;
|
|
41
|
+
|
|
42
|
+
for (const resolve of this.waiting.splice(0)) {
|
|
43
|
+
resolve({ done: true, value: undefined });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async next(): Promise<IteratorResult<T>> {
|
|
48
|
+
const value = this.values.shift();
|
|
49
|
+
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
return { done: false, value };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.closed) {
|
|
55
|
+
return { done: true, value: undefined };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
this.waiting.push(resolve);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
64
|
+
return {
|
|
65
|
+
next: () => this.next(),
|
|
66
|
+
return: async () => {
|
|
67
|
+
this.close();
|
|
68
|
+
return { done: true, value: undefined };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ClientToolCall {
|
|
75
|
+
readonly arguments: JsonObject;
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
readonly name: string,
|
|
79
|
+
args: JsonObject = {},
|
|
80
|
+
) {
|
|
81
|
+
this.arguments = args;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type WorkflowItem = Extract<ThreadItem, { type: "workflow" }>;
|
|
86
|
+
|
|
87
|
+
export class AgentContext<TContext> {
|
|
88
|
+
readonly thread: AgentContextOptions<TContext>["thread"];
|
|
89
|
+
readonly store: AgentContextOptions<TContext>["store"];
|
|
90
|
+
readonly context: TContext;
|
|
91
|
+
readonly previousResponseId: string | null;
|
|
92
|
+
workflowItem: WorkflowItem | null = null;
|
|
93
|
+
private readonly now: () => Date | string;
|
|
94
|
+
private readonly queue = new AsyncEventQueue<ThreadStreamEvent>();
|
|
95
|
+
private clientToolCall: ClientToolCall | null = null;
|
|
96
|
+
|
|
97
|
+
constructor(options: AgentContextOptions<TContext>) {
|
|
98
|
+
this.thread = options.thread;
|
|
99
|
+
this.store = options.store;
|
|
100
|
+
this.context = options.context;
|
|
101
|
+
this.previousResponseId = options.previousResponseId ?? null;
|
|
102
|
+
this.now = options.now ?? (() => new Date());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stream(event: ThreadStreamEvent): void {
|
|
106
|
+
this.queue.push(ThreadStreamEventSchema.parse(event));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async streamWidget(
|
|
110
|
+
widget: WidgetRoot | AsyncIterable<WidgetRoot>,
|
|
111
|
+
copyText?: string | null,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
for await (const event of streamWidgetEvents(this.thread, widget, {
|
|
114
|
+
copyText,
|
|
115
|
+
generateId: (itemType) => this.store.generateItemId(itemType, this.thread, this.context),
|
|
116
|
+
now: () => this.createdAt(),
|
|
117
|
+
})) {
|
|
118
|
+
this.stream(event);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
events(): AsyncIterable<ThreadStreamEvent> {
|
|
123
|
+
return this.queue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
closeEvents(): void {
|
|
127
|
+
this.queue.close();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
startWorkflow(workflow: Workflow): void {
|
|
131
|
+
const item = createWorkflowItem(this, workflow);
|
|
132
|
+
this.workflowItem = item;
|
|
133
|
+
|
|
134
|
+
if (shouldEmitWorkflowAdded(item.workflow)) {
|
|
135
|
+
this.stream(workflowAddedEvent(item));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
addWorkflowTask(task: Task): void {
|
|
140
|
+
const normalizedTask = normalizeWorkflowTask(task);
|
|
141
|
+
|
|
142
|
+
if (!this.workflowItem) {
|
|
143
|
+
this.workflowItem = createWorkflowItem(this, {
|
|
144
|
+
type: "custom",
|
|
145
|
+
tasks: [],
|
|
146
|
+
expanded: false,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const shouldEmitAdded =
|
|
151
|
+
this.workflowItem.workflow.type !== "reasoning" &&
|
|
152
|
+
this.workflowItem.workflow.tasks.length === 0;
|
|
153
|
+
const event = appendWorkflowTask(this.workflowItem, normalizedTask);
|
|
154
|
+
|
|
155
|
+
this.stream(shouldEmitAdded ? workflowAddedEvent(this.workflowItem) : event);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
updateWorkflowTask(task: Task, taskIndex: number): void {
|
|
159
|
+
if (!this.workflowItem) {
|
|
160
|
+
throw new Error("Workflow is not set");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.stream(updateWorkflowTaskEvent(this.workflowItem, task, taskIndex));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
endWorkflow(summary?: WorkflowSummary, expanded = false): void {
|
|
167
|
+
const event = finishWorkflow(this, summary, expanded);
|
|
168
|
+
|
|
169
|
+
if (event) {
|
|
170
|
+
this.stream(event);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setClientToolCall(toolCall: ClientToolCall): void {
|
|
175
|
+
if (this.clientToolCall) {
|
|
176
|
+
throw new Error("Only one client tool call can be set per response.");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.clientToolCall = toolCall;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getClientToolCall(): ClientToolCall | null {
|
|
183
|
+
return this.clientToolCall;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createdAt(): string {
|
|
187
|
+
const value = this.now();
|
|
188
|
+
return typeof value === "string" ? value : value.toISOString();
|
|
189
|
+
}
|
|
190
|
+
}
|