@agentica/core 0.32.2 → 0.32.3-dev.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/lib/index.mjs +320 -342
- package/lib/index.mjs.map +1 -1
- package/lib/orchestrate/call.js +87 -107
- package/lib/orchestrate/call.js.map +1 -1
- package/lib/orchestrate/describe.js +5 -50
- package/lib/orchestrate/describe.js.map +1 -1
- package/lib/orchestrate/initialize.js +5 -50
- package/lib/orchestrate/initialize.js.map +1 -1
- package/lib/orchestrate/select.js +107 -126
- package/lib/orchestrate/select.js.map +1 -1
- package/lib/utils/AssistantMessageEmptyError.d.ts +7 -0
- package/lib/utils/AssistantMessageEmptyError.js +17 -0
- package/lib/utils/AssistantMessageEmptyError.js.map +1 -0
- package/lib/utils/ChatGptCompletionStreamingUtil.d.ts +8 -0
- package/lib/utils/ChatGptCompletionStreamingUtil.js +86 -0
- package/lib/utils/ChatGptCompletionStreamingUtil.js.map +1 -0
- package/lib/utils/ChatGptCompletionStreamingUtil.spec.d.ts +1 -0
- package/lib/utils/ChatGptCompletionStreamingUtil.spec.js +855 -0
- package/lib/utils/ChatGptCompletionStreamingUtil.spec.js.map +1 -0
- package/lib/utils/MPSC.js +8 -6
- package/lib/utils/MPSC.js.map +1 -1
- package/lib/utils/StreamUtil.d.ts +1 -1
- package/lib/utils/StreamUtil.js +2 -2
- package/lib/utils/StreamUtil.js.map +1 -1
- package/lib/utils/__retry.d.ts +1 -0
- package/lib/utils/__retry.js +30 -0
- package/lib/utils/__retry.js.map +1 -0
- package/lib/utils/__retry.spec.d.ts +1 -0
- package/lib/utils/__retry.spec.js +172 -0
- package/lib/utils/__retry.spec.js.map +1 -0
- package/package.json +1 -1
- package/src/orchestrate/call.ts +88 -114
- package/src/orchestrate/describe.ts +7 -65
- package/src/orchestrate/initialize.ts +4 -64
- package/src/orchestrate/select.ts +111 -138
- package/src/utils/AssistantMessageEmptyError.ts +13 -0
- package/src/utils/ChatGptCompletionMessageUtil.ts +1 -1
- package/src/utils/ChatGptCompletionStreamingUtil.spec.ts +908 -0
- package/src/utils/ChatGptCompletionStreamingUtil.ts +90 -0
- package/src/utils/MPSC.ts +8 -6
- package/src/utils/StreamUtil.ts +2 -2
- package/src/utils/__retry.spec.ts +198 -0
- package/src/utils/__retry.ts +18 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ChatCompletion, ChatCompletionChunk } from "openai/resources";
|
|
2
|
+
|
|
3
|
+
import { ChatGptCompletionMessageUtil, MPSC, streamDefaultReaderToAsyncGenerator, StreamUtil, toAsyncGenerator } from ".";
|
|
4
|
+
|
|
5
|
+
async function reduceStreamingWithDispatch(stream: ReadableStream<ChatCompletionChunk>, eventProcessor: (props: {
|
|
6
|
+
stream: AsyncGenerator<string, undefined, undefined>;
|
|
7
|
+
done: () => boolean;
|
|
8
|
+
get: () => string;
|
|
9
|
+
join: () => Promise<string>;
|
|
10
|
+
}) => void) {
|
|
11
|
+
const streamContext = new Map<number, { content: string; mpsc: MPSC<string> }>();
|
|
12
|
+
|
|
13
|
+
const nullableCompletion = await StreamUtil.reduce<ChatCompletionChunk, Promise<ChatCompletion>>(stream, async (accPromise, chunk) => {
|
|
14
|
+
const acc = await accPromise;
|
|
15
|
+
const registerContext = (
|
|
16
|
+
choices: ChatCompletionChunk.Choice[],
|
|
17
|
+
) => {
|
|
18
|
+
for (const choice of choices) {
|
|
19
|
+
// Handle content first, even if finish_reason is present
|
|
20
|
+
if (choice.delta.content != null && choice.delta.content !== "") {
|
|
21
|
+
// Process content logic (moved up from below)
|
|
22
|
+
if (streamContext.has(choice.index)) {
|
|
23
|
+
const context = streamContext.get(choice.index)!;
|
|
24
|
+
context.content += choice.delta.content;
|
|
25
|
+
context.mpsc.produce(choice.delta.content);
|
|
26
|
+
} else {
|
|
27
|
+
const mpsc = new MPSC<string>();
|
|
28
|
+
|
|
29
|
+
streamContext.set(choice.index, {
|
|
30
|
+
content: choice.delta.content,
|
|
31
|
+
mpsc,
|
|
32
|
+
});
|
|
33
|
+
mpsc.produce(choice.delta.content);
|
|
34
|
+
|
|
35
|
+
eventProcessor({
|
|
36
|
+
stream: streamDefaultReaderToAsyncGenerator(mpsc.consumer.getReader()),
|
|
37
|
+
done: () => mpsc.done(),
|
|
38
|
+
get: () => streamContext.get(choice.index)?.content ?? "",
|
|
39
|
+
join: async () => {
|
|
40
|
+
await mpsc.waitClosed();
|
|
41
|
+
return streamContext.get(choice.index)!.content;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle finish_reason after content processing
|
|
48
|
+
if (choice.finish_reason != null) {
|
|
49
|
+
const context = streamContext.get(choice.index);
|
|
50
|
+
if (context != null) {
|
|
51
|
+
context.mpsc.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (acc.object === "chat.completion.chunk") {
|
|
57
|
+
registerContext([acc, chunk].flatMap(v => v.choices));
|
|
58
|
+
return ChatGptCompletionMessageUtil.merge([acc, chunk]);
|
|
59
|
+
}
|
|
60
|
+
registerContext(chunk.choices);
|
|
61
|
+
return ChatGptCompletionMessageUtil.accumulate(acc, chunk);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (nullableCompletion == null) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"StreamUtil.reduce did not produce a ChatCompletion. Possible causes: the input stream was empty, invalid, or closed prematurely. "
|
|
67
|
+
+ "To debug: check that the stream is properly initialized and contains valid ChatCompletionChunk data. "
|
|
68
|
+
+ "You may also enable verbose logging upstream to inspect the stream contents. "
|
|
69
|
+
+ `Stream locked: ${stream.locked}.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if((nullableCompletion.object as string) === "chat.completion.chunk") {
|
|
74
|
+
const completion = ChatGptCompletionMessageUtil.merge([nullableCompletion as unknown as ChatCompletionChunk]);
|
|
75
|
+
completion.choices.forEach((choice) => {
|
|
76
|
+
if(choice.message.content != null && choice.message.content !== "") {
|
|
77
|
+
eventProcessor({
|
|
78
|
+
stream: toAsyncGenerator(choice.message.content),
|
|
79
|
+
done: () => true,
|
|
80
|
+
get: () => choice.message.content!,
|
|
81
|
+
join: async () => choice.message.content!,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return completion;
|
|
86
|
+
}
|
|
87
|
+
return nullableCompletion;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { reduceStreamingWithDispatch };
|
package/src/utils/MPSC.ts
CHANGED
|
@@ -7,13 +7,15 @@ export class MPSC<T> {
|
|
|
7
7
|
public constructor() {
|
|
8
8
|
this.queue = new AsyncQueue<T>();
|
|
9
9
|
this.consumer = new ReadableStream<T>({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
start: async (controller) => {
|
|
11
|
+
while (true) {
|
|
12
|
+
const { value, done } = await this.queue.dequeue();
|
|
13
|
+
if (done === true) {
|
|
14
|
+
controller.close();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
controller.enqueue(value);
|
|
15
18
|
}
|
|
16
|
-
controller.enqueue(value);
|
|
17
19
|
},
|
|
18
20
|
});
|
|
19
21
|
}
|
package/src/utils/StreamUtil.ts
CHANGED
|
@@ -34,10 +34,10 @@ async function reduce<T, R = T>(stream: ReadableStream<T>, reducer: (acc: T | R,
|
|
|
34
34
|
return acc as R;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function from<T>(value: T): ReadableStream<T> {
|
|
37
|
+
function from<T>(...value: T[]): ReadableStream<T> {
|
|
38
38
|
const stream = new ReadableStream<T>({
|
|
39
39
|
start: (controller) => {
|
|
40
|
-
controller.enqueue(
|
|
40
|
+
value.forEach(v => controller.enqueue(v));
|
|
41
41
|
controller.close();
|
|
42
42
|
},
|
|
43
43
|
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { __get_retry } from "./__retry";
|
|
2
|
+
|
|
3
|
+
describe("__get_retry", () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.clearAllMocks();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe("success cases", () => {
|
|
9
|
+
it("should not retry when successful on first attempt", async () => {
|
|
10
|
+
const mockFn = vi.fn().mockResolvedValue("success");
|
|
11
|
+
const retryFn = __get_retry(3);
|
|
12
|
+
|
|
13
|
+
const result = await retryFn(mockFn);
|
|
14
|
+
|
|
15
|
+
expect(result).toBe("success");
|
|
16
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
17
|
+
expect(mockFn).toHaveBeenCalledWith(undefined);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should call exactly 2 times when successful on second attempt", async () => {
|
|
21
|
+
const mockFn = vi.fn()
|
|
22
|
+
.mockRejectedValueOnce(new Error("First failure"))
|
|
23
|
+
.mockResolvedValue("success");
|
|
24
|
+
const retryFn = __get_retry(3);
|
|
25
|
+
|
|
26
|
+
const result = await retryFn(mockFn);
|
|
27
|
+
|
|
28
|
+
expect(result).toBe("success");
|
|
29
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
30
|
+
expect(mockFn).toHaveBeenNthCalledWith(1, undefined);
|
|
31
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, new Error("First failure"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should call limit times when successful on last attempt", async () => {
|
|
35
|
+
const mockFn = vi.fn()
|
|
36
|
+
.mockRejectedValueOnce(new Error("First failure"))
|
|
37
|
+
.mockRejectedValueOnce(new Error("Second failure"))
|
|
38
|
+
.mockResolvedValue("success");
|
|
39
|
+
const retryFn = __get_retry(3);
|
|
40
|
+
|
|
41
|
+
const result = await retryFn(mockFn);
|
|
42
|
+
|
|
43
|
+
expect(result).toBe("success");
|
|
44
|
+
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
45
|
+
expect(mockFn).toHaveBeenNthCalledWith(1, undefined);
|
|
46
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, new Error("First failure"));
|
|
47
|
+
expect(mockFn).toHaveBeenNthCalledWith(3, new Error("Second failure"));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("failure cases", () => {
|
|
52
|
+
it("should throw last error after limit attempts", async () => {
|
|
53
|
+
const error1 = new Error("First failure");
|
|
54
|
+
const error2 = new Error("Second failure");
|
|
55
|
+
const error3 = new Error("Third failure");
|
|
56
|
+
|
|
57
|
+
const mockFn = vi.fn()
|
|
58
|
+
.mockRejectedValueOnce(error1)
|
|
59
|
+
.mockRejectedValueOnce(error2)
|
|
60
|
+
.mockRejectedValueOnce(error3);
|
|
61
|
+
const retryFn = __get_retry(3);
|
|
62
|
+
|
|
63
|
+
await expect(retryFn(mockFn)).rejects.toThrow("Third failure");
|
|
64
|
+
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
65
|
+
expect(mockFn).toHaveBeenNthCalledWith(1, undefined);
|
|
66
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, error1);
|
|
67
|
+
expect(mockFn).toHaveBeenNthCalledWith(3, error2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should throw error immediately when limit is 1", async () => {
|
|
71
|
+
const error = new Error("Immediate failure");
|
|
72
|
+
const mockFn = vi.fn().mockRejectedValue(error);
|
|
73
|
+
const retryFn = __get_retry(1);
|
|
74
|
+
|
|
75
|
+
await expect(retryFn(mockFn)).rejects.toThrow("Immediate failure");
|
|
76
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(mockFn).toHaveBeenCalledWith(undefined);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("prevError propagation", () => {
|
|
82
|
+
it("should pass previous error as prevError correctly", async () => {
|
|
83
|
+
const error1 = new Error("First error");
|
|
84
|
+
const error2 = new Error("Second error");
|
|
85
|
+
|
|
86
|
+
const mockFn = vi.fn()
|
|
87
|
+
.mockRejectedValueOnce(error1)
|
|
88
|
+
.mockRejectedValueOnce(error2)
|
|
89
|
+
.mockResolvedValue("success");
|
|
90
|
+
const retryFn = __get_retry(3);
|
|
91
|
+
|
|
92
|
+
const result = await retryFn(mockFn);
|
|
93
|
+
|
|
94
|
+
expect(result).toBe("success");
|
|
95
|
+
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
96
|
+
expect(mockFn).toHaveBeenNthCalledWith(1, undefined);
|
|
97
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, error1);
|
|
98
|
+
expect(mockFn).toHaveBeenNthCalledWith(3, error2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should use initial prevError in first call when provided", async () => {
|
|
102
|
+
const initialError = new Error("Initial error");
|
|
103
|
+
const mockFn = vi.fn().mockResolvedValue("success");
|
|
104
|
+
const retryFn = __get_retry(3);
|
|
105
|
+
|
|
106
|
+
const result = await retryFn(mockFn, initialError);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe("success");
|
|
109
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(mockFn).toHaveBeenCalledWith(initialError);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("different error types", () => {
|
|
115
|
+
it("should handle string errors correctly", async () => {
|
|
116
|
+
const mockFn = vi.fn()
|
|
117
|
+
.mockRejectedValueOnce("String error")
|
|
118
|
+
.mockResolvedValue("success");
|
|
119
|
+
const retryFn = __get_retry(2);
|
|
120
|
+
|
|
121
|
+
const result = await retryFn(mockFn);
|
|
122
|
+
|
|
123
|
+
expect(result).toBe("success");
|
|
124
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
125
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, "String error");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should handle null errors correctly", async () => {
|
|
129
|
+
const mockFn = vi.fn()
|
|
130
|
+
.mockRejectedValueOnce(null)
|
|
131
|
+
.mockResolvedValue("success");
|
|
132
|
+
const retryFn = __get_retry(2);
|
|
133
|
+
|
|
134
|
+
const result = await retryFn(mockFn);
|
|
135
|
+
|
|
136
|
+
expect(result).toBe("success");
|
|
137
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
138
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, null);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should handle undefined errors correctly", async () => {
|
|
142
|
+
const mockFn = vi.fn()
|
|
143
|
+
.mockRejectedValueOnce(undefined)
|
|
144
|
+
.mockResolvedValue("success");
|
|
145
|
+
const retryFn = __get_retry(2);
|
|
146
|
+
|
|
147
|
+
const result = await retryFn(mockFn);
|
|
148
|
+
|
|
149
|
+
expect(result).toBe("success");
|
|
150
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
151
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, undefined);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("recursive call verification", () => {
|
|
156
|
+
it("should occur recursive calls in correct order", async () => {
|
|
157
|
+
const callOrder: string[] = [];
|
|
158
|
+
const mockFn = vi.fn()
|
|
159
|
+
.mockImplementationOnce(() => {
|
|
160
|
+
callOrder.push("first call");
|
|
161
|
+
throw new Error("First failure");
|
|
162
|
+
})
|
|
163
|
+
.mockImplementationOnce(() => {
|
|
164
|
+
callOrder.push("second call");
|
|
165
|
+
throw new Error("Second failure");
|
|
166
|
+
})
|
|
167
|
+
.mockImplementationOnce(async () => {
|
|
168
|
+
callOrder.push("third call");
|
|
169
|
+
return Promise.resolve("success");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const retryFn = __get_retry(3);
|
|
173
|
+
const result = await retryFn(mockFn);
|
|
174
|
+
|
|
175
|
+
expect(result).toBe("success");
|
|
176
|
+
expect(callOrder).toEqual(["first call", "second call", "third call"]);
|
|
177
|
+
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("type safety", () => {
|
|
182
|
+
it("should handle different return types correctly", async () => {
|
|
183
|
+
const stringFn = vi.fn().mockResolvedValue("string result");
|
|
184
|
+
const numberFn = vi.fn().mockResolvedValue(42);
|
|
185
|
+
const objectFn = vi.fn().mockResolvedValue({ key: "value" });
|
|
186
|
+
|
|
187
|
+
const retryFn = __get_retry(3);
|
|
188
|
+
|
|
189
|
+
const stringResult = await retryFn(stringFn);
|
|
190
|
+
const numberResult = await retryFn(numberFn);
|
|
191
|
+
const objectResult = await retryFn(objectFn);
|
|
192
|
+
|
|
193
|
+
expect(stringResult).toBe("string result");
|
|
194
|
+
expect(numberResult).toBe(42);
|
|
195
|
+
expect(objectResult).toEqual({ key: "value" });
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*/
|
|
4
|
+
export function __get_retry(limit: number) {
|
|
5
|
+
const retryFn = async <T>(fn: (prevError?: unknown) => Promise<T>, prevError?: unknown, attempt: number = 0): Promise<T> => {
|
|
6
|
+
try {
|
|
7
|
+
return await fn(prevError);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (attempt >= limit - 1) {
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
return retryFn(fn, error, attempt + 1);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return retryFn;
|
|
18
|
+
}
|