@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,145 @@
1
+ import { match, P } from "ts-pattern";
2
+
3
+ import type { TokenUsage } from "@/core/provider/types.js";
4
+
5
+ /**
6
+ * Known trace operation types.
7
+ *
8
+ * Each `$` method registers a specific operation type in the execution
9
+ * trace. This discriminant allows consumers to filter or group trace
10
+ * entries by kind.
11
+ */
12
+ export type OperationType = "step" | "agent" | "map" | "each" | "reduce" | "while" | "all" | "race";
13
+
14
+ /** @deprecated Use `OperationType` instead. */
15
+ export type TraceType = OperationType;
16
+
17
+ /**
18
+ * A single entry in the execution trace.
19
+ *
20
+ * Every tracked `$` operation produces a TraceEntry. Nested operations
21
+ * (e.g. agent calls inside a map iteration) appear as children,
22
+ * forming a tree that represents the full execution graph.
23
+ *
24
+ * @internal
25
+ * Part of the internal execution context. Exposed on
26
+ * `WorkflowResult.trace` for observability but not directly
27
+ * constructed by user code.
28
+ */
29
+ export interface TraceEntry {
30
+ /**
31
+ * Unique id of this operation.
32
+ *
33
+ * Corresponds to the `id` field from the `$` config that
34
+ * produced this entry.
35
+ */
36
+ id: string;
37
+
38
+ /**
39
+ * What kind of operation produced this entry.
40
+ *
41
+ * Discriminant for filtering or grouping trace entries.
42
+ */
43
+ type: OperationType;
44
+
45
+ /**
46
+ * Input snapshot.
47
+ *
48
+ * Captured when the operation starts. May be `undefined` for
49
+ * operations that have no meaningful input (e.g. `$.all`).
50
+ */
51
+ input?: unknown;
52
+
53
+ /**
54
+ * Output snapshot.
55
+ *
56
+ * Captured when the operation completes successfully. `undefined`
57
+ * if the operation is still running or failed.
58
+ */
59
+ output?: unknown;
60
+
61
+ /**
62
+ * Start time in Unix milliseconds.
63
+ *
64
+ * Set when the operation begins execution.
65
+ */
66
+ startedAt: number;
67
+
68
+ /**
69
+ * End time in Unix milliseconds.
70
+ *
71
+ * Set when the operation completes (success or failure).
72
+ * `undefined` while the operation is still running.
73
+ */
74
+ finishedAt?: number;
75
+
76
+ /**
77
+ * Error instance if the operation failed.
78
+ *
79
+ * `undefined` on success or while still running.
80
+ */
81
+ error?: Error;
82
+
83
+ /**
84
+ * Token usage from this operation.
85
+ *
86
+ * Populated for `agent` type entries that complete successfully.
87
+ * `undefined` for non-agent steps or failed operations.
88
+ */
89
+ usage?: TokenUsage;
90
+
91
+ /**
92
+ * Nested trace entries for child operations.
93
+ *
94
+ * Present when this operation spawns sub-operations
95
+ * (e.g. individual iterations inside `$.map`, or nested
96
+ * `$.step` calls inside a step's `execute` callback).
97
+ */
98
+ children?: readonly TraceEntry[];
99
+ }
100
+
101
+ /**
102
+ * Recursively collect all {@link TokenUsage} values from a trace tree.
103
+ *
104
+ * Walks every entry (including nested children) and returns a flat
105
+ * array of usage objects. Entries without usage are skipped.
106
+ *
107
+ * @param trace - The trace array to collect from.
108
+ * @returns Flat array of {@link TokenUsage} values found in the tree.
109
+ */
110
+ export function collectUsages(trace: readonly TraceEntry[]): TokenUsage[] {
111
+ return trace.flatMap((entry) => {
112
+ const usages: TokenUsage[] = match(entry.usage)
113
+ .with(P.nonNullable, (u) => [u])
114
+ .otherwise(() => []);
115
+ if (entry.children != null && entry.children.length > 0) {
116
+ return [...usages, ...collectUsages(entry.children)];
117
+ }
118
+ return usages;
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Recursively deep-clone and freeze a trace array.
124
+ *
125
+ * Returns a structurally identical tree that is `Object.freeze`d at
126
+ * every level, preventing post-run mutation of the result trace.
127
+ *
128
+ * @internal
129
+ */
130
+ export function snapshotTrace(trace: readonly TraceEntry[]): readonly TraceEntry[] {
131
+ return Object.freeze(
132
+ trace.map((entry) => {
133
+ const children = match(entry.children)
134
+ .with(P.nonNullable, (c) => snapshotTrace(c))
135
+ .otherwise(() => undefined);
136
+ const childSpread = match(children)
137
+ .with(P.nonNullable, (c) => ({ children: c }))
138
+ .otherwise(() => ({}));
139
+ return Object.freeze({
140
+ ...entry,
141
+ ...childSpread,
142
+ });
143
+ }),
144
+ );
145
+ }
@@ -0,0 +1,123 @@
1
+ // ──────────────────────────────────────────────────────────────
2
+ // █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗
3
+ // ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝
4
+ // ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ███████╗
5
+ // ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║
6
+ // ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║
7
+ // ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
8
+ //
9
+ // AUTO-GENERATED — DO NOT EDIT
10
+ // Update: pnpm --filter=@pkg/agent-sdk generate:models
11
+ // ──────────────────────────────────────────────────────────────
12
+
13
+ import { match } from "ts-pattern";
14
+
15
+ import { MODELS as GENERATED_MODELS } from "./providers/index.js";
16
+
17
+ /**
18
+ * Supported OpenRouter model identifiers, derived from the generated {@link MODELS} array.
19
+ */
20
+ export type OpenRouterLanguageModelId = (typeof GENERATED_MODELS)[number]["id"];
21
+
22
+ /**
23
+ * Model category for classification and filtering.
24
+ */
25
+ export type ModelCategory = "chat" | "coding" | "reasoning";
26
+
27
+ /**
28
+ * Per-model pricing in USD per token.
29
+ *
30
+ * Field names match the OpenRouter API convention. All values are
31
+ * per-token (or per-unit) rates as numbers. Optional fields are
32
+ * omitted when the provider does not support them.
33
+ */
34
+ export interface ModelPricing {
35
+ /** Cost per input (prompt) token. */
36
+ prompt: number;
37
+
38
+ /** Cost per output (completion) token. */
39
+ completion: number;
40
+
41
+ /** Cost per cached input token (read). */
42
+ inputCacheRead?: number;
43
+
44
+ /** Cost per cached input token (write). */
45
+ inputCacheWrite?: number;
46
+
47
+ /** Cost per web search request. */
48
+ webSearch?: number;
49
+
50
+ /** Cost per internal reasoning token. */
51
+ internalReasoning?: number;
52
+
53
+ /** Cost per image input token. */
54
+ image?: number;
55
+
56
+ /** Cost per audio input second. */
57
+ audio?: number;
58
+
59
+ /** Cost per audio output second. */
60
+ audioOutput?: number;
61
+ }
62
+
63
+ /**
64
+ * Model definition with metadata and pricing.
65
+ */
66
+ export interface ModelDefinition {
67
+ /** OpenRouter model identifier (e.g. `"openai/gpt-5.2-codex"`). */
68
+ id: string;
69
+
70
+ /** Model category for classification. */
71
+ category: ModelCategory;
72
+
73
+ /** Token pricing rates. */
74
+ pricing: ModelPricing;
75
+ }
76
+
77
+ /**
78
+ * Supported OpenRouter models with pricing data.
79
+ */
80
+ export const MODELS = GENERATED_MODELS satisfies readonly ModelDefinition[];
81
+
82
+ /**
83
+ * Look up a model definition by its identifier.
84
+ *
85
+ * @param id - The model identifier to look up.
86
+ * @returns The matching model definition.
87
+ * @throws {Error} If no model matches the given ID.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const m = model('openai/gpt-5.2-codex')
92
+ * console.log(m.pricing.prompt) // 0.00000175
93
+ * console.log(m.category) // 'coding'
94
+ * ```
95
+ */
96
+ export function model(id: OpenRouterLanguageModelId): ModelDefinition {
97
+ const found = MODELS.find((m) => m.id === id);
98
+ if (!found) {
99
+ throw new Error(`Unknown model: ${id}`);
100
+ }
101
+ return found;
102
+ }
103
+
104
+ /**
105
+ * Return supported model definitions, optionally filtered.
106
+ *
107
+ * @param filter - Optional predicate to filter models.
108
+ * @returns A readonly array of matching model definitions.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const all = models()
113
+ * const reasoning = models((m) => m.category === 'reasoning')
114
+ * ```
115
+ */
116
+ export function models(filter?: (m: ModelDefinition) => boolean): readonly ModelDefinition[] {
117
+ return match(filter)
118
+ .when(
119
+ (f): f is (m: ModelDefinition) => boolean => f != null,
120
+ (f) => MODELS.filter(f),
121
+ )
122
+ .otherwise(() => MODELS);
123
+ }
@@ -0,0 +1,15 @@
1
+ // ──────────────────────────────────────────────────────────────
2
+ // █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗
3
+ // ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝
4
+ // ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ███████╗
5
+ // ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║
6
+ // ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║
7
+ // ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
8
+ //
9
+ // AUTO-GENERATED — DO NOT EDIT
10
+ // Update: pnpm --filter=@pkg/agent-sdk generate:models
11
+ // ──────────────────────────────────────────────────────────────
12
+
13
+ import { OPENAI_MODELS } from "./openai.js";
14
+
15
+ export const MODELS = [...OPENAI_MODELS] as const;
@@ -0,0 +1,84 @@
1
+ // ──────────────────────────────────────────────────────────────
2
+ // █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗
3
+ // ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝
4
+ // ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ███████╗
5
+ // ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║
6
+ // ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║
7
+ // ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
8
+ //
9
+ // AUTO-GENERATED — DO NOT EDIT
10
+ // Update: pnpm --filter=@pkg/agent-sdk generate:models
11
+ // ──────────────────────────────────────────────────────────────
12
+
13
+ export const OPENAI_MODELS = [
14
+ {
15
+ id: "openai/gpt-5.2-codex",
16
+ category: "coding",
17
+ pricing: { prompt: 0.00000175, completion: 0.000014, inputCacheRead: 1.75e-7, webSearch: 0.01 },
18
+ },
19
+ {
20
+ id: "openai/gpt-5.2",
21
+ category: "chat",
22
+ pricing: { prompt: 0.00000175, completion: 0.000014, inputCacheRead: 1.75e-7, webSearch: 0.01 },
23
+ },
24
+ {
25
+ id: "openai/gpt-5.1",
26
+ category: "chat",
27
+ pricing: { prompt: 0.00000125, completion: 0.00001, inputCacheRead: 1.25e-7, webSearch: 0.01 },
28
+ },
29
+ {
30
+ id: "openai/gpt-5",
31
+ category: "chat",
32
+ pricing: { prompt: 0.00000125, completion: 0.00001, inputCacheRead: 1.25e-7, webSearch: 0.01 },
33
+ },
34
+ {
35
+ id: "openai/gpt-5-mini",
36
+ category: "chat",
37
+ pricing: { prompt: 2.5e-7, completion: 0.000002, inputCacheRead: 2.5e-8, webSearch: 0.01 },
38
+ },
39
+ {
40
+ id: "openai/gpt-5-nano",
41
+ category: "chat",
42
+ pricing: { prompt: 5e-8, completion: 4e-7, inputCacheRead: 5e-9, webSearch: 0.01 },
43
+ },
44
+ {
45
+ id: "openai/gpt-4.1",
46
+ category: "chat",
47
+ pricing: { prompt: 0.000002, completion: 0.000008, inputCacheRead: 5e-7, webSearch: 0.01 },
48
+ },
49
+ {
50
+ id: "openai/gpt-4.1-mini",
51
+ category: "chat",
52
+ pricing: { prompt: 4e-7, completion: 0.0000016, inputCacheRead: 1e-7, webSearch: 0.01 },
53
+ },
54
+ {
55
+ id: "openai/gpt-4.1-nano",
56
+ category: "chat",
57
+ pricing: { prompt: 1e-7, completion: 4e-7, inputCacheRead: 2.5e-8, webSearch: 0.01 },
58
+ },
59
+ {
60
+ id: "openai/gpt-4o",
61
+ category: "chat",
62
+ pricing: { prompt: 0.0000025, completion: 0.00001, inputCacheRead: 0.00000125 },
63
+ },
64
+ {
65
+ id: "openai/gpt-4o-mini",
66
+ category: "chat",
67
+ pricing: { prompt: 1.5e-7, completion: 6e-7, inputCacheRead: 7.5e-8 },
68
+ },
69
+ {
70
+ id: "openai/o3",
71
+ category: "reasoning",
72
+ pricing: { prompt: 0.000002, completion: 0.000008, inputCacheRead: 5e-7, webSearch: 0.01 },
73
+ },
74
+ {
75
+ id: "openai/o3-mini",
76
+ category: "reasoning",
77
+ pricing: { prompt: 0.0000011, completion: 0.0000044, inputCacheRead: 5.5e-7 },
78
+ },
79
+ {
80
+ id: "openai/o4-mini",
81
+ category: "reasoning",
82
+ pricing: { prompt: 0.0000011, completion: 0.0000044, inputCacheRead: 2.75e-7, webSearch: 0.01 },
83
+ },
84
+ ] as const;
@@ -0,0 +1,32 @@
1
+ import type { Context, ExecutionContext } from "@/lib/context.js";
2
+ import { createMockLogger } from "@/testing/logger.js";
3
+
4
+ /**
5
+ * Create a mock {@link ExecutionContext} with a mock logger
6
+ * and a live (non-aborted) signal.
7
+ *
8
+ * Pass `overrides` to replace individual fields.
9
+ */
10
+ export function createMockExecutionCtx(overrides?: Partial<ExecutionContext>): ExecutionContext {
11
+ return {
12
+ signal: new AbortController().signal,
13
+ log: createMockLogger(),
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Create a mock {@link Context} with an empty trace, a mock logger,
20
+ * and a live (non-aborted) signal.
21
+ *
22
+ * Pass `overrides` to replace individual fields.
23
+ */
24
+ export function createMockCtx(overrides?: Partial<Context>): Context {
25
+ return {
26
+ signal: new AbortController().signal,
27
+ log: createMockLogger(),
28
+ trace: [],
29
+ messages: [],
30
+ ...overrides,
31
+ };
32
+ }
@@ -0,0 +1,2 @@
1
+ export { createMockLogger } from "@/testing/logger.js";
2
+ export { createMockCtx, createMockExecutionCtx } from "@/testing/context.js";
@@ -0,0 +1,19 @@
1
+ import { vi } from "vitest";
2
+
3
+ import type { Logger } from "@/core/logger.js";
4
+
5
+ /**
6
+ * Create a mock {@link Logger} backed by `vi.fn()` stubs.
7
+ *
8
+ * Every method is a no-op spy. `child()` returns a fresh mock
9
+ * logger so nested scopes work without blowing up.
10
+ */
11
+ export function createMockLogger(): Logger {
12
+ return {
13
+ debug: vi.fn(),
14
+ info: vi.fn(),
15
+ warn: vi.fn(),
16
+ error: vi.fn(),
17
+ child: vi.fn(() => createMockLogger()),
18
+ } as unknown as Logger;
19
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { attemptEach, attemptEachAsync } from "@/utils/attempt.js";
4
+
5
+ describe("attemptEach", () => {
6
+ it("returns success tuples for handlers that succeed", () => {
7
+ const results = attemptEach(
8
+ () => 1,
9
+ () => 2,
10
+ );
11
+ expect(results).toEqual([
12
+ [null, 1],
13
+ [null, 2],
14
+ ]);
15
+ });
16
+
17
+ it("returns error tuples for handlers that throw", () => {
18
+ const results = attemptEach(
19
+ () => {
20
+ throw new Error("boom");
21
+ },
22
+ () => 42,
23
+ );
24
+
25
+ expect(results).toHaveLength(2);
26
+ const first = results[0];
27
+ if (first == null) {
28
+ throw new Error("Expected first result to be defined");
29
+ }
30
+ expect(first[0]).toBeInstanceOf(Error);
31
+ expect((first[0] as Error).message).toBe("boom");
32
+ expect(first[1]).toBeNull();
33
+ expect(results[1]).toEqual([null, 42]);
34
+ });
35
+
36
+ it("skips undefined handlers", () => {
37
+ const results = attemptEach(
38
+ undefined,
39
+ () => "a",
40
+ undefined,
41
+ () => "b",
42
+ undefined,
43
+ );
44
+ expect(results).toEqual([
45
+ [null, "a"],
46
+ [null, "b"],
47
+ ]);
48
+ });
49
+
50
+ it("returns an empty array when all handlers are undefined", () => {
51
+ const results = attemptEach(undefined, undefined);
52
+ expect(results).toEqual([]);
53
+ });
54
+
55
+ it("returns an empty array when called with no arguments", () => {
56
+ const results = attemptEach();
57
+ expect(results).toEqual([]);
58
+ });
59
+ });
60
+
61
+ describe("attemptEachAsync", () => {
62
+ it("returns success tuples for async handlers that succeed", async () => {
63
+ const results = await attemptEachAsync(
64
+ async () => "a",
65
+ async () => "b",
66
+ );
67
+ expect(results).toEqual([
68
+ [null, "a"],
69
+ [null, "b"],
70
+ ]);
71
+ });
72
+
73
+ it("returns error tuples for async handlers that reject", async () => {
74
+ const results = await attemptEachAsync(
75
+ async () => {
76
+ throw new Error("async boom");
77
+ },
78
+ async () => "ok",
79
+ );
80
+
81
+ expect(results).toHaveLength(2);
82
+ const first = results[0];
83
+ if (first == null) {
84
+ throw new Error("Expected first result to be defined");
85
+ }
86
+ expect(first[0]).toBeInstanceOf(Error);
87
+ expect((first[0] as Error).message).toBe("async boom");
88
+ expect(first[1]).toBeNull();
89
+ expect(results[1]).toEqual([null, "ok"]);
90
+ });
91
+
92
+ it("handles sync handlers passed to the async variant", async () => {
93
+ const results = await attemptEachAsync(() => "sync");
94
+ expect(results).toEqual([[null, "sync"]]);
95
+ });
96
+
97
+ it("skips undefined handlers", async () => {
98
+ const results = await attemptEachAsync(undefined, async () => 1, undefined);
99
+ expect(results).toEqual([[null, 1]]);
100
+ });
101
+
102
+ it("returns an empty array when all handlers are undefined", async () => {
103
+ const results = await attemptEachAsync(undefined);
104
+ expect(results).toEqual([]);
105
+ });
106
+
107
+ it("returns an empty array when called with no arguments", async () => {
108
+ const results = await attemptEachAsync();
109
+ expect(results).toEqual([]);
110
+ });
111
+
112
+ it("executes handlers sequentially", async () => {
113
+ const order: number[] = [];
114
+ await attemptEachAsync(
115
+ async () => {
116
+ order.push(1);
117
+ },
118
+ async () => {
119
+ order.push(2);
120
+ },
121
+ async () => {
122
+ order.push(3);
123
+ },
124
+ );
125
+ expect(order).toEqual([1, 2, 3]);
126
+ });
127
+ });
@@ -0,0 +1,38 @@
1
+ import { attempt, attemptAsync } from "es-toolkit";
2
+
3
+ /**
4
+ * Run N callbacks sequentially, swallowing errors.
5
+ *
6
+ * Returns an array of `[error, result]` tuples following the
7
+ * same convention as `attempt` from es-toolkit.
8
+ *
9
+ * @param handlers - Callbacks to execute in order. `undefined` entries are skipped.
10
+ * @returns An array of attempt results for each non-undefined handler.
11
+ */
12
+ export function attemptEach<T = void, E = Error>(
13
+ ...handlers: Array<(() => T) | undefined>
14
+ ): Array<[null, T] | [E, null]> {
15
+ return handlers.filter((h): h is () => T => h != null).map((h) => attempt<T, E>(h));
16
+ }
17
+
18
+ /**
19
+ * Run N async callbacks sequentially, swallowing errors.
20
+ *
21
+ * Returns an array of `[error, result]` tuples following the
22
+ * same convention as `attemptAsync` from es-toolkit.
23
+ *
24
+ * @param handlers - Callbacks to execute in order. `undefined` entries are skipped.
25
+ * @returns An array of attempt results for each non-undefined handler.
26
+ */
27
+ export async function attemptEachAsync<T = void, E = Error>(
28
+ ...handlers: Array<(() => T | Promise<T>) | undefined>
29
+ ): Promise<Array<[null, T] | [E, null]>> {
30
+ const results: Array<[null, T] | [E, null]> = [];
31
+ for (const h of handlers) {
32
+ if (h != null) {
33
+ // oxlint-disable-next-line no-await-in-loop - sequential by design
34
+ results.push(await attemptAsync<T, E>(async () => h()));
35
+ }
36
+ }
37
+ return results;
38
+ }