@funkai/agents 0.1.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/.generated/req.txt +1 -0
- package/.turbo/turbo-build.log +21 -0
- package/.turbo/turbo-test$colon$coverage.log +109 -0
- package/.turbo/turbo-test.log +141 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +16 -0
- package/ISSUES.md +540 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/banner.svg +97 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/core/agents/base/agent.ts.html +1705 -0
- package/coverage/lcov-report/core/agents/base/index.html +146 -0
- package/coverage/lcov-report/core/agents/base/output.ts.html +256 -0
- package/coverage/lcov-report/core/agents/base/utils.ts.html +694 -0
- package/coverage/lcov-report/core/agents/flow/engine.ts.html +928 -0
- package/coverage/lcov-report/core/agents/flow/flow-agent.ts.html +1462 -0
- package/coverage/lcov-report/core/agents/flow/index.html +146 -0
- package/coverage/lcov-report/core/agents/flow/messages.ts.html +508 -0
- package/coverage/lcov-report/core/agents/flow/steps/factory.ts.html +1975 -0
- package/coverage/lcov-report/core/agents/flow/steps/index.html +116 -0
- package/coverage/lcov-report/core/index.html +131 -0
- package/coverage/lcov-report/core/logger.ts.html +541 -0
- package/coverage/lcov-report/core/models/providers/index.html +116 -0
- package/coverage/lcov-report/core/models/providers/openai.ts.html +337 -0
- package/coverage/lcov-report/core/provider/index.html +131 -0
- package/coverage/lcov-report/core/provider/provider.ts.html +346 -0
- package/coverage/lcov-report/core/provider/usage.ts.html +376 -0
- package/coverage/lcov-report/core/tool.ts.html +577 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +221 -0
- package/coverage/lcov-report/lib/hooks.ts.html +262 -0
- package/coverage/lcov-report/lib/index.html +161 -0
- package/coverage/lcov-report/lib/middleware.ts.html +274 -0
- package/coverage/lcov-report/lib/runnable.ts.html +151 -0
- package/coverage/lcov-report/lib/trace.ts.html +520 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/utils/attempt.ts.html +199 -0
- package/coverage/lcov-report/utils/error.ts.html +421 -0
- package/coverage/lcov-report/utils/index.html +176 -0
- package/coverage/lcov-report/utils/resolve.ts.html +208 -0
- package/coverage/lcov-report/utils/result.ts.html +538 -0
- package/coverage/lcov-report/utils/zod.ts.html +178 -0
- package/coverage/lcov.info +1566 -0
- package/dist/index.d.mts +2883 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2312 -0
- package/dist/index.mjs.map +1 -0
- package/docs/core/agent.md +231 -0
- package/docs/core/hooks.md +95 -0
- package/docs/core/overview.md +87 -0
- package/docs/core/step.md +279 -0
- package/docs/core/tools.md +98 -0
- package/docs/core/workflow.md +235 -0
- package/docs/guides/create-agent.md +224 -0
- package/docs/guides/create-tool.md +137 -0
- package/docs/guides/create-workflow.md +374 -0
- package/docs/overview.md +244 -0
- package/docs/provider/models.md +55 -0
- package/docs/provider/overview.md +106 -0
- package/docs/provider/usage.md +100 -0
- package/docs/research/experimental-context.md +167 -0
- package/docs/research/gap-analysis.md +86 -0
- package/docs/research/prepare-step-and-active-tools.md +138 -0
- package/docs/research/sub-agent-model.md +249 -0
- package/docs/troubleshooting.md +60 -0
- package/logo.svg +17 -0
- package/models.config.json +18 -0
- package/package.json +60 -0
- package/scripts/generate-models.ts +324 -0
- package/src/core/agents/base/agent.test.ts +1522 -0
- package/src/core/agents/base/agent.ts +547 -0
- package/src/core/agents/base/output.test.ts +93 -0
- package/src/core/agents/base/output.ts +57 -0
- package/src/core/agents/base/types.test-d.ts +69 -0
- package/src/core/agents/base/types.ts +503 -0
- package/src/core/agents/base/utils.test.ts +397 -0
- package/src/core/agents/base/utils.ts +197 -0
- package/src/core/agents/flow/engine.test.ts +452 -0
- package/src/core/agents/flow/engine.ts +281 -0
- package/src/core/agents/flow/flow-agent.test.ts +1027 -0
- package/src/core/agents/flow/flow-agent.ts +473 -0
- package/src/core/agents/flow/messages.test.ts +198 -0
- package/src/core/agents/flow/messages.ts +141 -0
- package/src/core/agents/flow/steps/agent.test.ts +280 -0
- package/src/core/agents/flow/steps/agent.ts +87 -0
- package/src/core/agents/flow/steps/all.test.ts +300 -0
- package/src/core/agents/flow/steps/all.ts +73 -0
- package/src/core/agents/flow/steps/builder.ts +124 -0
- package/src/core/agents/flow/steps/each.test.ts +257 -0
- package/src/core/agents/flow/steps/each.ts +61 -0
- package/src/core/agents/flow/steps/factory.test-d.ts +50 -0
- package/src/core/agents/flow/steps/factory.test.ts +1025 -0
- package/src/core/agents/flow/steps/factory.ts +645 -0
- package/src/core/agents/flow/steps/map.test.ts +273 -0
- package/src/core/agents/flow/steps/map.ts +75 -0
- package/src/core/agents/flow/steps/race.test.ts +290 -0
- package/src/core/agents/flow/steps/race.ts +59 -0
- package/src/core/agents/flow/steps/reduce.test.ts +310 -0
- package/src/core/agents/flow/steps/reduce.ts +73 -0
- package/src/core/agents/flow/steps/result.ts +27 -0
- package/src/core/agents/flow/steps/step.test.ts +402 -0
- package/src/core/agents/flow/steps/step.ts +51 -0
- package/src/core/agents/flow/steps/while.test.ts +283 -0
- package/src/core/agents/flow/steps/while.ts +75 -0
- package/src/core/agents/flow/types.ts +348 -0
- package/src/core/logger.test.ts +163 -0
- package/src/core/logger.ts +152 -0
- package/src/core/models/index.test.ts +137 -0
- package/src/core/models/index.ts +152 -0
- package/src/core/models/providers/openai.ts +84 -0
- package/src/core/provider/provider.test.ts +128 -0
- package/src/core/provider/provider.ts +99 -0
- package/src/core/provider/types.ts +98 -0
- package/src/core/provider/usage.test.ts +304 -0
- package/src/core/provider/usage.ts +97 -0
- package/src/core/tool.test.ts +65 -0
- package/src/core/tool.ts +164 -0
- package/src/core/types.ts +66 -0
- package/src/index.ts +95 -0
- package/src/lib/context.test.ts +86 -0
- package/src/lib/context.ts +49 -0
- package/src/lib/hooks.test.ts +102 -0
- package/src/lib/hooks.ts +59 -0
- package/src/lib/middleware.test.ts +122 -0
- package/src/lib/middleware.ts +63 -0
- package/src/lib/runnable.test.ts +41 -0
- package/src/lib/runnable.ts +22 -0
- package/src/lib/trace.test.ts +291 -0
- package/src/lib/trace.ts +145 -0
- package/src/models/index.ts +123 -0
- package/src/models/providers/index.ts +15 -0
- package/src/models/providers/openai.ts +84 -0
- package/src/testing/context.ts +32 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/logger.ts +19 -0
- package/src/utils/attempt.test.ts +127 -0
- package/src/utils/attempt.ts +38 -0
- package/src/utils/error.test.ts +179 -0
- package/src/utils/error.ts +112 -0
- package/src/utils/resolve.test.ts +38 -0
- package/src/utils/resolve.ts +41 -0
- package/src/utils/result.test.ts +79 -0
- package/src/utils/result.ts +151 -0
- package/src/utils/zod.test.ts +69 -0
- package/src/utils/zod.ts +31 -0
- package/tsconfig.json +25 -0
- package/tsdown.config.ts +15 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { toError, safeStringify, safeStringifyJSON } from "@/utils/error.js";
|
|
4
|
+
|
|
5
|
+
describe("toError", () => {
|
|
6
|
+
it("returns Error instances as-is", () => {
|
|
7
|
+
const original = new Error("boom");
|
|
8
|
+
const result = toError(original);
|
|
9
|
+
expect(result).toBe(original);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("preserves Error subclasses", () => {
|
|
13
|
+
const original = new TypeError("bad type");
|
|
14
|
+
const result = toError(original);
|
|
15
|
+
expect(result).toBe(original);
|
|
16
|
+
expect(result).toBeInstanceOf(TypeError);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("wraps a string into an Error", () => {
|
|
20
|
+
const result = toError("raw string");
|
|
21
|
+
expect(result).toBeInstanceOf(Error);
|
|
22
|
+
expect(result.message).toBe("raw string");
|
|
23
|
+
expect(result.cause).toBe("raw string");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("serializes a plain object as JSON", () => {
|
|
27
|
+
const thrown = { status: 400, message: "sandbox name too long" };
|
|
28
|
+
const result = toError(thrown);
|
|
29
|
+
expect(result).toBeInstanceOf(Error);
|
|
30
|
+
expect(result.message).toBe('{"status":400,"message":"sandbox name too long"}');
|
|
31
|
+
expect(result.cause).toBe(thrown);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("serializes an array as JSON", () => {
|
|
35
|
+
const thrown = ["error1", "error2"];
|
|
36
|
+
const result = toError(thrown);
|
|
37
|
+
expect(result).toBeInstanceOf(Error);
|
|
38
|
+
expect(result.message).toBe('["error1","error2"]');
|
|
39
|
+
expect(result.cause).toBe(thrown);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("serializes a Map as entries array", () => {
|
|
43
|
+
const thrown = new Map([["key", "value"]]);
|
|
44
|
+
const result = toError(thrown);
|
|
45
|
+
expect(result).toBeInstanceOf(Error);
|
|
46
|
+
expect(result.message).toBe('[["key","value"]]');
|
|
47
|
+
expect(result.cause).toBe(thrown);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("serializes a Set as values array", () => {
|
|
51
|
+
const thrown = new Set([1, 2, 3]);
|
|
52
|
+
const result = toError(thrown);
|
|
53
|
+
expect(result).toBeInstanceOf(Error);
|
|
54
|
+
expect(result.message).toBe("[1,2,3]");
|
|
55
|
+
expect(result.cause).toBe(thrown);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("handles circular references without throwing", () => {
|
|
59
|
+
const thrown: Record<string, unknown> = { name: "circular" };
|
|
60
|
+
thrown.self = thrown;
|
|
61
|
+
const result = toError(thrown);
|
|
62
|
+
expect(result).toBeInstanceOf(Error);
|
|
63
|
+
// Falls back to String() when JSON.stringify throws on circular refs
|
|
64
|
+
expect(result.message).toBe("[object Object]");
|
|
65
|
+
expect(result.cause).toBe(thrown);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("handles null", () => {
|
|
69
|
+
const result = toError(null);
|
|
70
|
+
expect(result).toBeInstanceOf(Error);
|
|
71
|
+
expect(result.message).toBe("null");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("handles undefined", () => {
|
|
75
|
+
const result = toError(undefined);
|
|
76
|
+
expect(result).toBeInstanceOf(Error);
|
|
77
|
+
expect(result.message).toBe("undefined");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles numbers", () => {
|
|
81
|
+
const result = toError(42);
|
|
82
|
+
expect(result).toBeInstanceOf(Error);
|
|
83
|
+
expect(result.message).toBe("42");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("handles booleans", () => {
|
|
87
|
+
const result = toError(false);
|
|
88
|
+
expect(result).toBeInstanceOf(Error);
|
|
89
|
+
expect(result.message).toBe("false");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("safeStringify", () => {
|
|
94
|
+
it("stringifies a plain object as JSON", () => {
|
|
95
|
+
expect(safeStringify({ status: 400 })).toBe('{"status":400}');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("stringifies an array as JSON", () => {
|
|
99
|
+
expect(safeStringify([1, 2, 3])).toBe("[1,2,3]");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("stringifies a Map as entries", () => {
|
|
103
|
+
expect(safeStringify(new Map([["k", "v"]]))).toBe('[["k","v"]]');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stringifies a Set as values", () => {
|
|
107
|
+
expect(safeStringify(new Set(["a", "b"]))).toBe('["a","b"]');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("stringifies null", () => {
|
|
111
|
+
expect(safeStringify(null)).toBe("null");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("stringifies undefined", () => {
|
|
115
|
+
expect(safeStringify(undefined)).toBe("undefined");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("stringifies a number", () => {
|
|
119
|
+
expect(safeStringify(42)).toBe("42");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("stringifies a boolean", () => {
|
|
123
|
+
expect(safeStringify(true)).toBe("true");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("stringifies a string as-is", () => {
|
|
127
|
+
expect(safeStringify("hello")).toBe("hello");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("falls back to String() for circular references", () => {
|
|
131
|
+
const circular: Record<string, unknown> = { name: "loop" };
|
|
132
|
+
circular.self = circular;
|
|
133
|
+
expect(safeStringify(circular)).toBe("[object Object]");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("safeStringifyJSON", () => {
|
|
138
|
+
it("serializes a plain object", () => {
|
|
139
|
+
expect(safeStringifyJSON({ a: 1, b: "two" })).toBe('{"a":1,"b":"two"}');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("serializes an array", () => {
|
|
143
|
+
expect(safeStringifyJSON([1, 2, 3])).toBe("[1,2,3]");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("serializes a Map as entries", () => {
|
|
147
|
+
expect(safeStringifyJSON(new Map([["k", "v"]]))).toBe('[["k","v"]]');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("serializes a Set as values", () => {
|
|
151
|
+
expect(safeStringifyJSON(new Set(["a", "b"]))).toBe('["a","b"]');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("serializes a string", () => {
|
|
155
|
+
expect(safeStringifyJSON("hello")).toBe('"hello"');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("serializes null", () => {
|
|
159
|
+
expect(safeStringifyJSON(null)).toBe("null");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("serializes a number", () => {
|
|
163
|
+
expect(safeStringifyJSON(42)).toBe("42");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns empty string for circular references", () => {
|
|
167
|
+
const circular: Record<string, unknown> = { name: "loop" };
|
|
168
|
+
circular.self = circular;
|
|
169
|
+
expect(safeStringifyJSON(circular)).toBe("");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns empty string for undefined input", () => {
|
|
173
|
+
expect(safeStringifyJSON(undefined)).toBe("");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("returns empty string for functions", () => {
|
|
177
|
+
expect(safeStringifyJSON(() => "nope")).toBe("");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { attempt, isError, isMap, isNil, isPrimitive, isSet, isString } from "es-toolkit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Coerces an unknown thrown value into a proper `Error` instance.
|
|
5
|
+
*
|
|
6
|
+
* Handles the common cases where libraries throw non-`Error` values
|
|
7
|
+
* (e.g. plain API response bodies, arrays, Maps) that would otherwise
|
|
8
|
+
* serialize as `[object Object]` in error messages.
|
|
9
|
+
*
|
|
10
|
+
* @param thrown - The caught value from a `catch` block.
|
|
11
|
+
* @returns An `Error` with a meaningful `.message`. If `thrown` is
|
|
12
|
+
* already an `Error`, it is returned as-is. The original value is
|
|
13
|
+
* preserved as `.cause` for debugging.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* try {
|
|
18
|
+
* await riskyCall()
|
|
19
|
+
* } catch (thrown) {
|
|
20
|
+
* const error = toError(thrown)
|
|
21
|
+
* console.error(error.message)
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function toError(thrown: unknown): Error {
|
|
26
|
+
if (isError(thrown)) {
|
|
27
|
+
return thrown;
|
|
28
|
+
}
|
|
29
|
+
if (isString(thrown)) {
|
|
30
|
+
return new Error(thrown, { cause: thrown });
|
|
31
|
+
}
|
|
32
|
+
return new Error(safeStringify(thrown), { cause: thrown });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Produce a human-readable string from any unknown value.
|
|
37
|
+
*
|
|
38
|
+
* Uses `JSON.stringify` for structured types (plain objects, arrays)
|
|
39
|
+
* so the message contains actual content instead of `[object Object]`.
|
|
40
|
+
* Maps and Sets are converted to their array representation first.
|
|
41
|
+
* Falls back to `String()` for primitives or when serialization fails
|
|
42
|
+
* (e.g. circular references).
|
|
43
|
+
*
|
|
44
|
+
* @param value - The value to stringify.
|
|
45
|
+
* @returns A meaningful string representation.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* safeStringify({ status: 400 }) // '{"status":400}'
|
|
50
|
+
* safeStringify(new Map([['k', 'v']])) // '[["k","v"]]'
|
|
51
|
+
* safeStringify(null) // 'null'
|
|
52
|
+
* safeStringify(42) // '42'
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function safeStringify(value: unknown): string {
|
|
56
|
+
if (isNil(value) || isPrimitive(value)) {
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
return safeStringifyJSON(value) || String(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Safely serializes a value to a JSON string without throwing.
|
|
64
|
+
*
|
|
65
|
+
* Converts types that `JSON.stringify` handles poorly (Maps, Sets)
|
|
66
|
+
* into serializable equivalents before stringifying. Returns an empty
|
|
67
|
+
* string when serialization fails (e.g. circular references).
|
|
68
|
+
*
|
|
69
|
+
* @param value - The value to serialize.
|
|
70
|
+
* @returns The JSON string, or an empty string if serialization fails.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* safeStringifyJSON({ status: 400 }) // '{"status":400}'
|
|
75
|
+
* safeStringifyJSON(new Map([['k', 'v']])) // '[["k","v"]]'
|
|
76
|
+
* safeStringifyJSON(circularObj) // ''
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function safeStringifyJSON(value: unknown): string {
|
|
80
|
+
const serializable = toSerializable(value);
|
|
81
|
+
const [error, json] = attempt(() => JSON.stringify(serializable) as string | undefined);
|
|
82
|
+
if (!isNil(error) || isNil(json)) {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
return json;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// private helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert types that `JSON.stringify` handles poorly into
|
|
94
|
+
* serializable equivalents.
|
|
95
|
+
*
|
|
96
|
+
* - `Map` → array of `[key, value]` entries
|
|
97
|
+
* - `Set` → array of values
|
|
98
|
+
* - Everything else → passed through unchanged
|
|
99
|
+
*
|
|
100
|
+
* @private
|
|
101
|
+
* @param value - The value to normalize.
|
|
102
|
+
* @returns A JSON-friendly representation.
|
|
103
|
+
*/
|
|
104
|
+
function toSerializable(value: unknown): unknown {
|
|
105
|
+
if (isMap(value)) {
|
|
106
|
+
return Array.from(value.entries());
|
|
107
|
+
}
|
|
108
|
+
if (isSet(value)) {
|
|
109
|
+
return Array.from(value);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { resolve } from "@/utils/resolve.js";
|
|
4
|
+
|
|
5
|
+
describe("resolve", () => {
|
|
6
|
+
it("returns a static value as-is", async () => {
|
|
7
|
+
const result = await resolve("hello", {});
|
|
8
|
+
expect(result).toBe("hello");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns undefined when value is undefined", async () => {
|
|
12
|
+
const result = await resolve(undefined, {});
|
|
13
|
+
expect(result).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("calls a sync function with ctx and returns the result", async () => {
|
|
17
|
+
const result = await resolve((ctx: { name: string }) => `hi ${ctx.name}`, { name: "Ada" });
|
|
18
|
+
expect(result).toBe("hi Ada");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("calls an async function with ctx and returns the resolved result", async () => {
|
|
22
|
+
const result = await resolve(async (ctx: { n: number }) => ctx.n * 2, { n: 21 });
|
|
23
|
+
expect(result).toBe(42);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns static non-string values (number, object)", async () => {
|
|
27
|
+
expect(await resolve(42, {})).toBe(42);
|
|
28
|
+
expect(await resolve({ key: "value" }, {})).toEqual({ key: "value" });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("propagates errors from the resolver function", async () => {
|
|
32
|
+
await expect(
|
|
33
|
+
resolve(() => {
|
|
34
|
+
throw new Error("resolver boom");
|
|
35
|
+
}, {}),
|
|
36
|
+
).rejects.toThrow("resolver boom");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isFunction } from "es-toolkit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A value that is either static or dynamically resolved from context.
|
|
5
|
+
*
|
|
6
|
+
* Use this for agent/workflow definition fields that may depend on
|
|
7
|
+
* runtime state (e.g. tools, instructions, prompt).
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T - The resolved value type.
|
|
10
|
+
* @typeParam TCtx - The context type passed when resolving dynamically.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Static
|
|
15
|
+
* instructions: 'You are a helpful assistant'
|
|
16
|
+
*
|
|
17
|
+
* // Dynamic (sync)
|
|
18
|
+
* instructions: (ctx) => `Analyze ${ctx.input.repoName}`
|
|
19
|
+
*
|
|
20
|
+
* // Dynamic (async)
|
|
21
|
+
* instructions: async (ctx) => fetchPrompt(ctx.input.repoName)
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export type ResolveParam<T, TCtx> = T | ((ctx: TCtx) => T | Promise<T>);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a {@link ResolveParam} value — if it's a function, call it with ctx.
|
|
28
|
+
*
|
|
29
|
+
* @param value - A static value, a function of context, or undefined.
|
|
30
|
+
* @param ctx - The context to pass if the value is a function.
|
|
31
|
+
* @returns The resolved value, or `undefined` if the input was `undefined`.
|
|
32
|
+
*/
|
|
33
|
+
export async function resolve<T, TCtx>(
|
|
34
|
+
value: ResolveParam<T, TCtx> | undefined,
|
|
35
|
+
ctx: TCtx,
|
|
36
|
+
): Promise<T | undefined> {
|
|
37
|
+
if (isFunction(value)) {
|
|
38
|
+
return value(ctx);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { ok, err, isOk, isErr } from "@/utils/result.js";
|
|
4
|
+
import type { Result } from "@/utils/result.js";
|
|
5
|
+
|
|
6
|
+
describe("ok", () => {
|
|
7
|
+
it("creates a success result with ok: true", () => {
|
|
8
|
+
const result = ok({ output: "hello" });
|
|
9
|
+
expect(result.ok).toBe(true);
|
|
10
|
+
expect(result.output).toBe("hello");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("spreads all fields flat onto the result", () => {
|
|
14
|
+
const result = ok({ output: "text", messages: [], duration: 42 });
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
ok: true,
|
|
17
|
+
output: "text",
|
|
18
|
+
messages: [],
|
|
19
|
+
duration: 42,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("err", () => {
|
|
25
|
+
it("creates a failure result with ok: false", () => {
|
|
26
|
+
const result = err("VALIDATION_ERROR", "Name is required");
|
|
27
|
+
expect(result.ok).toBe(false);
|
|
28
|
+
expect(result.error.code).toBe("VALIDATION_ERROR");
|
|
29
|
+
expect(result.error.message).toBe("Name is required");
|
|
30
|
+
expect(result.error.cause).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("includes the cause when provided", () => {
|
|
34
|
+
const cause = new Error("root cause");
|
|
35
|
+
const result = err("AGENT_ERROR", "something broke", cause);
|
|
36
|
+
expect(result.error.cause).toBe(cause);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isOk", () => {
|
|
41
|
+
it("returns true for success results", () => {
|
|
42
|
+
const result: Result<{ value: number }> = ok({ value: 1 });
|
|
43
|
+
expect(isOk(result)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns false for failure results", () => {
|
|
47
|
+
const result: Result<{ value: number }> = err("ERR", "fail");
|
|
48
|
+
expect(isOk(result)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("narrows the type so success fields are accessible", () => {
|
|
52
|
+
const result: Result<{ value: number }> = ok({ value: 42 });
|
|
53
|
+
if (isOk(result)) {
|
|
54
|
+
// This line would fail to compile if narrowing didn't work
|
|
55
|
+
expect(result.value).toBe(42);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("isErr", () => {
|
|
61
|
+
it("returns true for failure results", () => {
|
|
62
|
+
const result: Result<{ value: number }> = err("ERR", "fail");
|
|
63
|
+
expect(isErr(result)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns false for success results", () => {
|
|
67
|
+
const result: Result<{ value: number }> = ok({ value: 1 });
|
|
68
|
+
expect(isErr(result)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("narrows the type so error fields are accessible", () => {
|
|
72
|
+
const result: Result<{ value: number }> = err("MY_CODE", "my message");
|
|
73
|
+
if (isErr(result)) {
|
|
74
|
+
// This line would fail to compile if narrowing didn't work
|
|
75
|
+
expect(result.error.code).toBe("MY_CODE");
|
|
76
|
+
expect(result.error.message).toBe("my message");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error information returned when an SDK operation fails.
|
|
3
|
+
*
|
|
4
|
+
* Every public method returns `Result<T>` instead of throwing.
|
|
5
|
+
* When `ok` is `false`, the error details are available on this interface.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResultError {
|
|
8
|
+
/**
|
|
9
|
+
* Machine-readable error code.
|
|
10
|
+
*
|
|
11
|
+
* Identifies the category of failure. Stable across versions —
|
|
12
|
+
* safe to match against in application code.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* switch (result.error.code) {
|
|
17
|
+
* case 'VALIDATION_ERROR': // input schema failed
|
|
18
|
+
* case 'ABORT_ERROR': // signal was aborted
|
|
19
|
+
* case 'AGENT_ERROR': // agent execution failed
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
code: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Human-readable error description.
|
|
27
|
+
*
|
|
28
|
+
* Suitable for logging but not for programmatic matching —
|
|
29
|
+
* use `code` for that.
|
|
30
|
+
*/
|
|
31
|
+
message: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Original thrown error, if any.
|
|
35
|
+
*
|
|
36
|
+
* Preserved so callers can inspect the root cause when the
|
|
37
|
+
* SDK catches and wraps an exception.
|
|
38
|
+
*/
|
|
39
|
+
cause?: Error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Discriminated union for SDK operation results.
|
|
44
|
+
*
|
|
45
|
+
* Success fields are **flat on the object** — no `.value` wrapper.
|
|
46
|
+
* Callers pattern-match on `ok` instead of using try/catch.
|
|
47
|
+
*
|
|
48
|
+
* @typeParam T - The success payload shape. All fields from `T` are
|
|
49
|
+
* spread directly onto the success branch alongside `ok: true`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const result = await agent.generate({ topic: 'TypeScript' })
|
|
54
|
+
*
|
|
55
|
+
* if (!result.ok) {
|
|
56
|
+
* // Error branch — only `ok` and `error` are present.
|
|
57
|
+
* console.error(result.error.code, result.error.message)
|
|
58
|
+
* return
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* // Success branch — all fields from T are directly on result.
|
|
62
|
+
* console.log(result.output)
|
|
63
|
+
* console.log(result.duration)
|
|
64
|
+
* console.log(result.trace)
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export type Result<T> = (T & { ok: true }) | { ok: false; error: ResultError };
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Constructors
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a success `Result`.
|
|
75
|
+
*
|
|
76
|
+
* Spreads the payload flat onto the object alongside `ok: true`.
|
|
77
|
+
*
|
|
78
|
+
* @param value - The success payload.
|
|
79
|
+
* @returns A success `Result<T>`.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* return ok({ output: 'hello', messages: [] })
|
|
84
|
+
* // → { ok: true, output: 'hello', messages: [] }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function ok<T extends Record<string, unknown>>(value: T): T & { ok: true } {
|
|
88
|
+
return { ...value, ok: true as const };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a failure `Result`.
|
|
93
|
+
*
|
|
94
|
+
* @param code - Machine-readable error code.
|
|
95
|
+
* @param message - Human-readable error description.
|
|
96
|
+
* @param cause - Optional original thrown error.
|
|
97
|
+
* @returns A failure `Result` for any `T`.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* return err('VALIDATION_ERROR', 'Name is required')
|
|
102
|
+
* return err('AGENT_ERROR', error.message, error)
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function err(
|
|
106
|
+
code: string,
|
|
107
|
+
message: string,
|
|
108
|
+
cause?: Error,
|
|
109
|
+
): { ok: false; error: ResultError } {
|
|
110
|
+
return { ok: false as const, error: { code, message, cause } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Type guards
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Narrow a `Result<T>` to its success branch.
|
|
119
|
+
*
|
|
120
|
+
* @param result - The result to check.
|
|
121
|
+
* @returns `true` when `result.ok` is `true`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const result = await agent.generate('hello')
|
|
126
|
+
* if (isOk(result)) {
|
|
127
|
+
* console.log(result.output)
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function isOk<T>(result: Result<T>): result is T & { ok: true } {
|
|
132
|
+
return result.ok;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Narrow a `Result<T>` to its failure branch.
|
|
137
|
+
*
|
|
138
|
+
* @param result - The result to check.
|
|
139
|
+
* @returns `true` when `result.ok` is `false`.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const result = await agent.generate('hello')
|
|
144
|
+
* if (isErr(result)) {
|
|
145
|
+
* console.error(result.error.code, result.error.message)
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function isErr<T>(result: Result<T>): result is { ok: false; error: ResultError } {
|
|
150
|
+
return !result.ok;
|
|
151
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { isZodArray, isZodObject, toJsonSchema } from "@/utils/zod.js";
|
|
5
|
+
|
|
6
|
+
describe("toJsonSchema", () => {
|
|
7
|
+
it("converts an object schema to JSON Schema", () => {
|
|
8
|
+
const schema = z.object({ name: z.string() });
|
|
9
|
+
const result = toJsonSchema(schema);
|
|
10
|
+
|
|
11
|
+
expect(result.type).toBe("object");
|
|
12
|
+
expect(result.properties).toHaveProperty("name");
|
|
13
|
+
expect(result.required).toContain("name");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("converts an array schema to JSON Schema", () => {
|
|
17
|
+
const schema = z.array(z.number());
|
|
18
|
+
const result = toJsonSchema(schema);
|
|
19
|
+
|
|
20
|
+
expect(result.type).toBe("array");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("converts a string schema to JSON Schema", () => {
|
|
24
|
+
const result = toJsonSchema(z.string());
|
|
25
|
+
expect(result.type).toBe("string");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("converts a number schema to JSON Schema", () => {
|
|
29
|
+
const result = toJsonSchema(z.number());
|
|
30
|
+
expect(result.type).toBe("number");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("converts a boolean schema to JSON Schema", () => {
|
|
34
|
+
const result = toJsonSchema(z.boolean());
|
|
35
|
+
expect(result.type).toBe("boolean");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("isZodObject", () => {
|
|
40
|
+
it("returns true for object schemas", () => {
|
|
41
|
+
expect(isZodObject(z.object({ x: z.number() }))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns false for array schemas", () => {
|
|
45
|
+
expect(isZodObject(z.array(z.string()))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns false for primitive schemas", () => {
|
|
49
|
+
expect(isZodObject(z.string())).toBe(false);
|
|
50
|
+
expect(isZodObject(z.number())).toBe(false);
|
|
51
|
+
expect(isZodObject(z.boolean())).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("isZodArray", () => {
|
|
56
|
+
it("returns true for array schemas", () => {
|
|
57
|
+
expect(isZodArray(z.array(z.string()))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns false for object schemas", () => {
|
|
61
|
+
expect(isZodArray(z.object({ x: z.number() }))).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns false for primitive schemas", () => {
|
|
65
|
+
expect(isZodArray(z.string())).toBe(false);
|
|
66
|
+
expect(isZodArray(z.number())).toBe(false);
|
|
67
|
+
expect(isZodArray(z.boolean())).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/utils/zod.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
type JSONSchema = z.core.JSONSchema.JSONSchema;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert a Zod schema to a JSON Schema object.
|
|
8
|
+
*/
|
|
9
|
+
export function toJsonSchema(schema: ZodType): JSONSchema {
|
|
10
|
+
return z.toJSONSchema(schema);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a Zod schema produces a JSON Schema with `type: 'object'`.
|
|
15
|
+
*
|
|
16
|
+
* Uses JSON Schema output rather than `instanceof` to correctly handle
|
|
17
|
+
* wrapped schemas (transforms, refinements, pipes).
|
|
18
|
+
*/
|
|
19
|
+
export function isZodObject(schema: ZodType): boolean {
|
|
20
|
+
return toJsonSchema(schema).type === "object";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a Zod schema produces a JSON Schema with `type: 'array'`.
|
|
25
|
+
*
|
|
26
|
+
* Uses JSON Schema output rather than `instanceof` to correctly handle
|
|
27
|
+
* wrapped schemas (transforms, refinements, pipes).
|
|
28
|
+
*/
|
|
29
|
+
export function isZodArray(schema: ZodType): boolean {
|
|
30
|
+
return toJsonSchema(schema).type === "array";
|
|
31
|
+
}
|