@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.
Files changed (153) hide show
  1. package/.generated/req.txt +1 -0
  2. package/.turbo/turbo-build.log +21 -0
  3. package/.turbo/turbo-test$colon$coverage.log +109 -0
  4. package/.turbo/turbo-test.log +141 -0
  5. package/.turbo/turbo-typecheck.log +4 -0
  6. package/CHANGELOG.md +16 -0
  7. package/ISSUES.md +540 -0
  8. package/LICENSE +21 -0
  9. package/README.md +128 -0
  10. package/banner.svg +97 -0
  11. package/coverage/lcov-report/base.css +224 -0
  12. package/coverage/lcov-report/block-navigation.js +87 -0
  13. package/coverage/lcov-report/core/agents/base/agent.ts.html +1705 -0
  14. package/coverage/lcov-report/core/agents/base/index.html +146 -0
  15. package/coverage/lcov-report/core/agents/base/output.ts.html +256 -0
  16. package/coverage/lcov-report/core/agents/base/utils.ts.html +694 -0
  17. package/coverage/lcov-report/core/agents/flow/engine.ts.html +928 -0
  18. package/coverage/lcov-report/core/agents/flow/flow-agent.ts.html +1462 -0
  19. package/coverage/lcov-report/core/agents/flow/index.html +146 -0
  20. package/coverage/lcov-report/core/agents/flow/messages.ts.html +508 -0
  21. package/coverage/lcov-report/core/agents/flow/steps/factory.ts.html +1975 -0
  22. package/coverage/lcov-report/core/agents/flow/steps/index.html +116 -0
  23. package/coverage/lcov-report/core/index.html +131 -0
  24. package/coverage/lcov-report/core/logger.ts.html +541 -0
  25. package/coverage/lcov-report/core/models/providers/index.html +116 -0
  26. package/coverage/lcov-report/core/models/providers/openai.ts.html +337 -0
  27. package/coverage/lcov-report/core/provider/index.html +131 -0
  28. package/coverage/lcov-report/core/provider/provider.ts.html +346 -0
  29. package/coverage/lcov-report/core/provider/usage.ts.html +376 -0
  30. package/coverage/lcov-report/core/tool.ts.html +577 -0
  31. package/coverage/lcov-report/favicon.png +0 -0
  32. package/coverage/lcov-report/index.html +221 -0
  33. package/coverage/lcov-report/lib/hooks.ts.html +262 -0
  34. package/coverage/lcov-report/lib/index.html +161 -0
  35. package/coverage/lcov-report/lib/middleware.ts.html +274 -0
  36. package/coverage/lcov-report/lib/runnable.ts.html +151 -0
  37. package/coverage/lcov-report/lib/trace.ts.html +520 -0
  38. package/coverage/lcov-report/prettify.css +1 -0
  39. package/coverage/lcov-report/prettify.js +2 -0
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +210 -0
  42. package/coverage/lcov-report/utils/attempt.ts.html +199 -0
  43. package/coverage/lcov-report/utils/error.ts.html +421 -0
  44. package/coverage/lcov-report/utils/index.html +176 -0
  45. package/coverage/lcov-report/utils/resolve.ts.html +208 -0
  46. package/coverage/lcov-report/utils/result.ts.html +538 -0
  47. package/coverage/lcov-report/utils/zod.ts.html +178 -0
  48. package/coverage/lcov.info +1566 -0
  49. package/dist/index.d.mts +2883 -0
  50. package/dist/index.d.mts.map +1 -0
  51. package/dist/index.mjs +2312 -0
  52. package/dist/index.mjs.map +1 -0
  53. package/docs/core/agent.md +231 -0
  54. package/docs/core/hooks.md +95 -0
  55. package/docs/core/overview.md +87 -0
  56. package/docs/core/step.md +279 -0
  57. package/docs/core/tools.md +98 -0
  58. package/docs/core/workflow.md +235 -0
  59. package/docs/guides/create-agent.md +224 -0
  60. package/docs/guides/create-tool.md +137 -0
  61. package/docs/guides/create-workflow.md +374 -0
  62. package/docs/overview.md +244 -0
  63. package/docs/provider/models.md +55 -0
  64. package/docs/provider/overview.md +106 -0
  65. package/docs/provider/usage.md +100 -0
  66. package/docs/research/experimental-context.md +167 -0
  67. package/docs/research/gap-analysis.md +86 -0
  68. package/docs/research/prepare-step-and-active-tools.md +138 -0
  69. package/docs/research/sub-agent-model.md +249 -0
  70. package/docs/troubleshooting.md +60 -0
  71. package/logo.svg +17 -0
  72. package/models.config.json +18 -0
  73. package/package.json +60 -0
  74. package/scripts/generate-models.ts +324 -0
  75. package/src/core/agents/base/agent.test.ts +1522 -0
  76. package/src/core/agents/base/agent.ts +547 -0
  77. package/src/core/agents/base/output.test.ts +93 -0
  78. package/src/core/agents/base/output.ts +57 -0
  79. package/src/core/agents/base/types.test-d.ts +69 -0
  80. package/src/core/agents/base/types.ts +503 -0
  81. package/src/core/agents/base/utils.test.ts +397 -0
  82. package/src/core/agents/base/utils.ts +197 -0
  83. package/src/core/agents/flow/engine.test.ts +452 -0
  84. package/src/core/agents/flow/engine.ts +281 -0
  85. package/src/core/agents/flow/flow-agent.test.ts +1027 -0
  86. package/src/core/agents/flow/flow-agent.ts +473 -0
  87. package/src/core/agents/flow/messages.test.ts +198 -0
  88. package/src/core/agents/flow/messages.ts +141 -0
  89. package/src/core/agents/flow/steps/agent.test.ts +280 -0
  90. package/src/core/agents/flow/steps/agent.ts +87 -0
  91. package/src/core/agents/flow/steps/all.test.ts +300 -0
  92. package/src/core/agents/flow/steps/all.ts +73 -0
  93. package/src/core/agents/flow/steps/builder.ts +124 -0
  94. package/src/core/agents/flow/steps/each.test.ts +257 -0
  95. package/src/core/agents/flow/steps/each.ts +61 -0
  96. package/src/core/agents/flow/steps/factory.test-d.ts +50 -0
  97. package/src/core/agents/flow/steps/factory.test.ts +1025 -0
  98. package/src/core/agents/flow/steps/factory.ts +645 -0
  99. package/src/core/agents/flow/steps/map.test.ts +273 -0
  100. package/src/core/agents/flow/steps/map.ts +75 -0
  101. package/src/core/agents/flow/steps/race.test.ts +290 -0
  102. package/src/core/agents/flow/steps/race.ts +59 -0
  103. package/src/core/agents/flow/steps/reduce.test.ts +310 -0
  104. package/src/core/agents/flow/steps/reduce.ts +73 -0
  105. package/src/core/agents/flow/steps/result.ts +27 -0
  106. package/src/core/agents/flow/steps/step.test.ts +402 -0
  107. package/src/core/agents/flow/steps/step.ts +51 -0
  108. package/src/core/agents/flow/steps/while.test.ts +283 -0
  109. package/src/core/agents/flow/steps/while.ts +75 -0
  110. package/src/core/agents/flow/types.ts +348 -0
  111. package/src/core/logger.test.ts +163 -0
  112. package/src/core/logger.ts +152 -0
  113. package/src/core/models/index.test.ts +137 -0
  114. package/src/core/models/index.ts +152 -0
  115. package/src/core/models/providers/openai.ts +84 -0
  116. package/src/core/provider/provider.test.ts +128 -0
  117. package/src/core/provider/provider.ts +99 -0
  118. package/src/core/provider/types.ts +98 -0
  119. package/src/core/provider/usage.test.ts +304 -0
  120. package/src/core/provider/usage.ts +97 -0
  121. package/src/core/tool.test.ts +65 -0
  122. package/src/core/tool.ts +164 -0
  123. package/src/core/types.ts +66 -0
  124. package/src/index.ts +95 -0
  125. package/src/lib/context.test.ts +86 -0
  126. package/src/lib/context.ts +49 -0
  127. package/src/lib/hooks.test.ts +102 -0
  128. package/src/lib/hooks.ts +59 -0
  129. package/src/lib/middleware.test.ts +122 -0
  130. package/src/lib/middleware.ts +63 -0
  131. package/src/lib/runnable.test.ts +41 -0
  132. package/src/lib/runnable.ts +22 -0
  133. package/src/lib/trace.test.ts +291 -0
  134. package/src/lib/trace.ts +145 -0
  135. package/src/models/index.ts +123 -0
  136. package/src/models/providers/index.ts +15 -0
  137. package/src/models/providers/openai.ts +84 -0
  138. package/src/testing/context.ts +32 -0
  139. package/src/testing/index.ts +2 -0
  140. package/src/testing/logger.ts +19 -0
  141. package/src/utils/attempt.test.ts +127 -0
  142. package/src/utils/attempt.ts +38 -0
  143. package/src/utils/error.test.ts +179 -0
  144. package/src/utils/error.ts +112 -0
  145. package/src/utils/resolve.test.ts +38 -0
  146. package/src/utils/resolve.ts +41 -0
  147. package/src/utils/result.test.ts +79 -0
  148. package/src/utils/result.ts +151 -0
  149. package/src/utils/zod.test.ts +69 -0
  150. package/src/utils/zod.ts +31 -0
  151. package/tsconfig.json +25 -0
  152. package/tsdown.config.ts +15 -0
  153. 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
+ });
@@ -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
+ }