@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
package/ISSUES.md ADDED
@@ -0,0 +1,540 @@
1
+ # Agent SDK — Issue Log
2
+
3
+ Tracked issues discovered during code review. Each issue is tagged with severity, category, and acceptance criteria for resolution.
4
+
5
+ ---
6
+
7
+ <issue id="1" severity="high" category="bug" status="closed">
8
+
9
+ ## #1 — StepResult spread fails for non-object types
10
+
11
+ **File:** `src/core/workflows/steps/factory.ts:152`
12
+ **Related:** `src/core/workflows/steps/result.ts:26-28`
13
+
14
+ ### Description
15
+
16
+ `executeStep` constructs the success result as:
17
+
18
+ ```typescript
19
+ return { ok: true, ...(value as T), step: stepInfo, duration } as StepResult<T>;
20
+ ```
21
+
22
+ Spreading a primitive (`string`, `number`, `boolean`) produces garbage at runtime. `..."hello"` yields `{0:'h',1:'e',...}`. The `StepResult<T>` type definition (`T & { ok: true; ... }`) is also unsound for primitives since `string & { ok: true }` is effectively `never`.
23
+
24
+ Any step returning a non-object value (`$.step<string>(...)`, `$.step<number>(...)`) will produce broken results that the type system masks.
25
+
26
+ ### Acceptance Criteria
27
+
28
+ - [ ] `StepResult<T>` wraps the success value in a named field (e.g. `value: T`) instead of intersecting `T &`
29
+ - [ ] OR `T` is constrained to `Record<string, unknown>` to enforce object-only step results
30
+ - [ ] Existing tests updated to reflect the new shape
31
+ - [ ] A test exists that verifies `$.step<string>(...)` returns the correct value
32
+
33
+ </issue>
34
+
35
+ ---
36
+
37
+ <issue id="2" severity="high" category="bug" status="closed">
38
+
39
+ ## #2 — Abort signal not propagated to agents in workflow steps
40
+
41
+ **File:** `src/core/workflows/steps/factory.ts:182-186`
42
+
43
+ ### Description
44
+
45
+ When `$.agent()` calls `config.agent.generate(config.input, agentConfig)`, the `agentConfig` merges user config and logger but never includes `ctx.signal`:
46
+
47
+ ```typescript
48
+ const agentConfig = {
49
+ ...config.config,
50
+ logger: ctx.log.child({ stepId: config.id }),
51
+ // Missing: signal: ctx.signal
52
+ };
53
+ ```
54
+
55
+ If the workflow is cancelled via abort signal, agents running inside `$.agent()` steps continue until completion.
56
+
57
+ ### Acceptance Criteria
58
+
59
+ - [ ] `$.agent()` passes `ctx.signal` to the agent via `agentConfig.signal`
60
+ - [ ] User-provided `config.config.signal` takes precedence over `ctx.signal` if explicitly set
61
+ - [ ] A test verifies that aborting the workflow signal causes the agent call to receive the signal
62
+
63
+ </issue>
64
+
65
+ ---
66
+
67
+ <issue id="3" severity="high" category="bug" status="closed">
68
+
69
+ ## #3 — Agent `stream()` eagerly consumes entire stream
70
+
71
+ **File:** `src/core/agent/agent.ts:274-303`
72
+
73
+ ### Description
74
+
75
+ The `stream()` method fully drains `aiResult.textStream` before returning:
76
+
77
+ ```typescript
78
+ const chunks: string[] = [];
79
+ for await (const chunk of aiResult.textStream) {
80
+ chunks.push(chunk);
81
+ }
82
+ ```
83
+
84
+ Then creates a "replay stream" from collected chunks. The caller cannot consume text incrementally — `stream()` is functionally identical to `generate()` with extra overhead.
85
+
86
+ ### Acceptance Criteria
87
+
88
+ - [ ] `stream()` returns before the full generation completes
89
+ - [ ] The returned stream emits chunks as they arrive from the model
90
+ - [ ] `output` and `messages` are resolved after the stream completes (e.g. via promises or post-stream access)
91
+ - [ ] Existing `stream()` tests updated to verify incremental delivery
92
+
93
+ </issue>
94
+
95
+ ---
96
+
97
+ <issue id="4" severity="medium" category="bug" status="closed">
98
+
99
+ ## #4 — Agent `stream()` ignores structured output
100
+
101
+ **File:** `src/core/agent/agent.ts:280-282`
102
+
103
+ ### Description
104
+
105
+ In `stream()`, the output is always set to raw text:
106
+
107
+ ```typescript
108
+ const finalText = await aiResult.text;
109
+ const finalOutput = finalText;
110
+ ```
111
+
112
+ Compare with `generate()` which correctly branches:
113
+
114
+ ```typescript
115
+ output: (output ? aiResult.output : aiResult.text) as TOutput;
116
+ ```
117
+
118
+ When an agent has structured output (e.g. `Output.object({ schema })`), `stream()` returns a raw string instead of the parsed object. The `TOutput` type assertion masks this at compile time.
119
+
120
+ ### Acceptance Criteria
121
+
122
+ - [ ] `stream()` checks for structured output the same way `generate()` does
123
+ - [ ] When `output` is configured, `stream()` returns the parsed object, not raw text
124
+ - [ ] A test exists that verifies `stream()` with structured output returns the correct type
125
+
126
+ </issue>
127
+
128
+ ---
129
+
130
+ <issue id="5" severity="medium" category="bug" status="closed">
131
+
132
+ ## #5 — `$.race()` does not cancel losing entries
133
+
134
+ **File:** `src/core/workflows/steps/factory.ts:331-343`
135
+ **Related:** `src/core/workflows/steps/race.ts:5`
136
+
137
+ ### Description
138
+
139
+ `RaceConfig` docs say "Losers are cancelled via abort signal" but the implementation is just:
140
+
141
+ ```typescript
142
+ execute: async () => Promise.race(config.entries),
143
+ ```
144
+
145
+ No abort controller is created. No signals are propagated. Losing entries continue running to completion.
146
+
147
+ ### Acceptance Criteria
148
+
149
+ - [ ] `$.race()` cancels losing entries when the winner resolves
150
+ - [ ] OR the docs are updated to remove the cancellation claim and document actual behavior
151
+ - [ ] If cancellation is implemented, entries must accept an abort signal (API change to accept factories instead of pre-started promises)
152
+
153
+ </issue>
154
+
155
+ ---
156
+
157
+ <issue id="6" severity="medium" category="bug" status="closed">
158
+
159
+ ## #6 — `$.all()` / `$.race()` entries are pre-started promises
160
+
161
+ **File:** `src/core/workflows/steps/factory.ts:313-329`
162
+ **Related:** `src/core/workflows/steps/all.ts`, `src/core/workflows/steps/race.ts`
163
+
164
+ ### Description
165
+
166
+ `AllConfig.entries` and `RaceConfig.entries` are typed as `Promise<any>[]`. By the time `executeStep` records `startedAt`, those promises are already executing. This means:
167
+
168
+ 1. Trace `startedAt` timestamp is too late (work already began)
169
+ 2. If entries resolved before `$.all()` is called, hooks fire after the fact
170
+ 3. `duration` measurement underestimates actual execution time
171
+
172
+ The `AllConfig` JSDoc acknowledges this ("already tracked individually") but the `executeStep` wrapper's timing is still misleading.
173
+
174
+ ### Acceptance Criteria
175
+
176
+ - [ ] Entries are changed to factory functions `(() => Promise<T>)[]` so the framework controls start time
177
+ - [ ] OR the trace/timing for `$.all()` / `$.race()` is documented as "coordination overhead only, not total execution time"
178
+ - [ ] If factories are adopted, update `StepBuilder` types and all call sites
179
+
180
+ </issue>
181
+
182
+ ---
183
+
184
+ <issue id="7" severity="medium" category="logic" status="closed">
185
+
186
+ ## #7 — `onStepFinish` never fires on error
187
+
188
+ **File:** `src/core/workflows/steps/factory.ts:167-168`
189
+
190
+ ### Description
191
+
192
+ The catch block only fires step-level `onError`. The workflow-level `parentHooks.onStepFinish` is never called for failed steps. This is intentional per the comment, but asymmetric with `onStepStart` (which always fires). There is no `onStepError` hook at the workflow level.
193
+
194
+ Consumers using `onStepFinish` for telemetry (e.g. recording step durations) will silently miss all failed steps.
195
+
196
+ ### Acceptance Criteria
197
+
198
+ - [ ] `onStepFinish` fires for both success and error cases (with error info included in the event)
199
+ - [ ] OR a new workflow-level `onStepError` hook is added
200
+ - [ ] The chosen behavior is documented in the `WorkflowConfig` JSDoc
201
+
202
+ </issue>
203
+
204
+ ---
205
+
206
+ <issue id="8" severity="medium" category="logic" status="closed">
207
+
208
+ ## #8 — `finishReason` passed as `stepId` in agent `onStepFinish` hook
209
+
210
+ **File:** `src/core/agent/agent.ts:144, 265`
211
+
212
+ ### Description
213
+
214
+ Both `generate()` and `stream()` pass the AI SDK's `event.finishReason` as the `stepId`:
215
+
216
+ ```typescript
217
+ onStepFinish: async (event) => {
218
+ config.onStepFinish!({ stepId: event.finishReason });
219
+ };
220
+ ```
221
+
222
+ `finishReason` is `"stop"`, `"length"`, `"tool-calls"`, etc. — not a step identifier. Consumers expecting a unique step ID will get the finish reason instead.
223
+
224
+ ### Acceptance Criteria
225
+
226
+ - [ ] Pass a meaningful step identifier (e.g. counter-based ID, or `agentName:stepIndex`)
227
+ - [ ] OR rename the hook parameter from `stepId` to `finishReason` to match the actual value
228
+ - [ ] Update the `AgentConfig.onStepFinish` type signature accordingly
229
+
230
+ </issue>
231
+
232
+ ---
233
+
234
+ <issue id="9" severity="low" category="logic" status="closed">
235
+
236
+ ## #9 — `withModelMiddleware` wraps model even with empty middleware
237
+
238
+ **File:** `src/lib/middleware.ts:49-52`
239
+
240
+ ### Description
241
+
242
+ When devtools is disabled (production) and no user middleware is provided, the function still calls `wrapLanguageModel({ model, middleware: [] })`. This creates an unnecessary wrapper layer.
243
+
244
+ ### Acceptance Criteria
245
+
246
+ - [ ] Return the model directly when middleware array is empty
247
+ - [ ] Add a test verifying no wrapping occurs in production with no middleware
248
+
249
+ </issue>
250
+
251
+ ---
252
+
253
+ <issue id="10" severity="low" category="logic" status="closed">
254
+
255
+ ## #10 — Middleware ordering contradicts documentation
256
+
257
+ **File:** `src/lib/middleware.ts:13-14, 45-46`
258
+
259
+ ### Description
260
+
261
+ JSDoc says "Additional middleware to apply **after** defaults" but the implementation puts user middleware first:
262
+
263
+ ```typescript
264
+ [...options.middleware, ...defaultMiddleware];
265
+ ```
266
+
267
+ User middleware is outermost (wraps around devtools), contradicting "after defaults."
268
+
269
+ ### Acceptance Criteria
270
+
271
+ - [ ] Either swap the order to `[...defaultMiddleware, ...options.middleware]`
272
+ - [ ] OR update the JSDoc to say "before defaults" / "outermost"
273
+ - [ ] Add a test that verifies middleware execution order
274
+
275
+ </issue>
276
+
277
+ ---
278
+
279
+ <issue id="11" severity="low" category="improvement" status="closed">
280
+
281
+ ## #11 — `snapshotTrace` shallow-clones entries
282
+
283
+ **File:** `src/lib/trace.ts:100`
284
+
285
+ ### Description
286
+
287
+ `{ ...entry }` only clones the top-level `TraceEntry` fields. `entry.input`, `entry.output`, and `entry.error` are reference copies. Mutations to original objects after snapshot are visible through the "frozen" trace.
288
+
289
+ ### Acceptance Criteria
290
+
291
+ - [ ] Use `structuredClone` for input/output fields (noting `Error` objects need special handling)
292
+ - [ ] OR document that input/output references are shared and only the trace structure is frozen
293
+
294
+ </issue>
295
+
296
+ ---
297
+
298
+ <issue id="12" severity="medium" category="improvement" status="closed">
299
+
300
+ ## #12 — No abort signal checking in sequential step loops
301
+
302
+ **File:** `src/core/workflows/steps/factory.ts:246-298`
303
+
304
+ ### Description
305
+
306
+ `$.each()` (line 246), `$.reduce()` (line 266), and `$.while()` (line 293) all loop without checking `ctx.signal.aborted`. If a workflow is cancelled mid-loop, these operations continue iterating through all items. `$.while()` is especially vulnerable since it loops on an arbitrary condition.
307
+
308
+ ### Acceptance Criteria
309
+
310
+ - [ ] Each sequential loop checks `ctx.signal.aborted` at the start of each iteration
311
+ - [ ] When aborted, the loop throws an appropriate error (e.g. `AbortError`)
312
+ - [ ] A test verifies that aborting mid-loop stops iteration
313
+
314
+ </issue>
315
+
316
+ ---
317
+
318
+ <issue id="13" severity="low" category="improvement" status="closed">
319
+
320
+ ## #13 — `poolMap` ignores abort signal
321
+
322
+ **File:** `src/core/workflows/steps/factory.ts:371-376`
323
+
324
+ ### Description
325
+
326
+ The `poolMap` worker loop (`while (nextIndex < items.length)`) never checks for abort signal cancellation. Workers continue processing items after the workflow has been aborted.
327
+
328
+ ### Acceptance Criteria
329
+
330
+ - [ ] `poolMap` accepts the abort signal and checks it before starting each item
331
+ - [ ] When aborted, workers exit cleanly
332
+
333
+ </issue>
334
+
335
+ ---
336
+
337
+ <issue id="14" severity="low" category="improvement" status="closed">
338
+
339
+ ## #14 — `writer.write()` not awaited in workflow stream
340
+
341
+ **File:** `src/core/workflows/workflow.ts:470-472`
342
+
343
+ ### Description
344
+
345
+ The `emit` function calls `writer.write(event)` without awaiting the returned promise. If the readable side is cancelled or the internal queue is full, the rejection is unhandled. Additionally, `emit` is typed as synchronous `(event: StepEvent) => void` but `writer.write` is async.
346
+
347
+ ### Acceptance Criteria
348
+
349
+ - [ ] Either await the write (and change `emit` to async)
350
+ - [ ] OR add `.catch(() => {})` to swallow write errors silently
351
+ - [ ] The `emit` type signature matches the actual behavior
352
+
353
+ </issue>
354
+
355
+ ---
356
+
357
+ <issue id="15" severity="low" category="improvement" status="closed">
358
+
359
+ ## #15 — `openrouter()` creates a new provider instance on every call
360
+
361
+ **File:** `src/core/provider/provider.ts:47-51`
362
+
363
+ ### Description
364
+
365
+ Every call to `openrouter(modelId)` creates a new `OpenRouterProvider` instance. If the provider maintains internal state, caching, or connection pooling, this is wasteful.
366
+
367
+ ### Acceptance Criteria
368
+
369
+ - [ ] Cache the provider instance lazily at module scope
370
+ - [ ] Invalidation strategy if `OPENROUTER_API_KEY` changes at runtime (or document that it doesn't)
371
+
372
+ </issue>
373
+
374
+ ---
375
+
376
+ <issue id="16" severity="low" category="improvement" status="closed">
377
+
378
+ ## #16 — `createTool` adds unnecessary async wrapper around execute
379
+
380
+ **File:** `src/core/tool.ts:109`
381
+
382
+ ### Description
383
+
384
+ `execute: async (data: TInput) => config.execute(data)` wraps the user's execute function in an extra async layer, adding one unnecessary microtask per tool call. The wrapper exists so `assertTool` can validate the intermediate object, but `config.execute` could be passed directly.
385
+
386
+ ### Acceptance Criteria
387
+
388
+ - [ ] Pass `config.execute` directly instead of wrapping
389
+ - [ ] OR document why the wrapper is intentional (if there's a reason beyond assertion)
390
+
391
+ </issue>
392
+
393
+ ---
394
+
395
+ <issue id="17" severity="medium" category="improvement" status="closed">
396
+
397
+ ## #17 — Subagent tool calls do not propagate abort signal
398
+
399
+ **File:** `src/core/agent/utils.ts:54-74`
400
+
401
+ ### Description
402
+
403
+ When a subagent is wrapped as a tool via `buildAITools`, the `execute` function calls `runnable.generate(input)` without passing an abort signal. The AI SDK passes `{ abortSignal }` as the second parameter to tool execute functions, but this code ignores it. If the parent agent is cancelled, subagent tool calls continue running.
404
+
405
+ ### Acceptance Criteria
406
+
407
+ - [ ] Accept the `abortSignal` from the AI SDK tool execution context
408
+ - [ ] Forward it as `{ signal: abortSignal }` to `runnable.generate()`
409
+ - [ ] A test verifies signal propagation from parent to subagent
410
+
411
+ </issue>
412
+
413
+ ---
414
+
415
+ <issue id="18" severity="low" category="improvement" status="closed">
416
+
417
+ ## #18 — No runtime guard for `input` schema without `prompt` function
418
+
419
+ **File:** `src/core/agent/utils.ts:107`
420
+
421
+ ### Description
422
+
423
+ If a user provides `input` schema but omits `prompt` (or vice versa), the code silently falls through to simple mode. Both fields are independently optional in `AgentConfig` — no type-level or runtime enforcement that they must be provided together.
424
+
425
+ ### Acceptance Criteria
426
+
427
+ - [ ] Add a runtime warning or error when `input` is provided without `prompt` (and vice versa)
428
+ - [ ] OR use a discriminated union at the type level to enforce the constraint
429
+ - [ ] A test verifies the guard triggers correctly
430
+
431
+ </issue>
432
+
433
+ ---
434
+
435
+ <issue id="19" severity="low" category="improvement" status="closed">
436
+
437
+ ## #19 — `LanguageModel` type narrowed to `specificationVersion: 'v3'` only
438
+
439
+ **File:** `src/core/provider/types.ts:11`
440
+
441
+ ### Description
442
+
443
+ ```typescript
444
+ export type LanguageModel = Extract<BaseLanguageModel, { specificationVersion: "v3" }>;
445
+ ```
446
+
447
+ This rejects models using future spec versions (v4, etc.). When the AI SDK introduces a new version, models using it won't be assignable. Forward-incompatible.
448
+
449
+ ### Acceptance Criteria
450
+
451
+ - [ ] Use the base `LanguageModel` type directly
452
+ - [ ] OR use a union that accommodates known + future versions
453
+ - [ ] OR document this as intentional pinning with a note to update on AI SDK major bumps
454
+
455
+ </issue>
456
+
457
+ ---
458
+
459
+ <issue id="20" severity="low" category="improvement" status="closed">
460
+
461
+ ## #20 — Duck-typing for `OutputSpec` detection is fragile
462
+
463
+ **File:** `src/core/agent/output.ts:37-40`
464
+
465
+ ### Description
466
+
467
+ `resolveOutput` checks `'parseCompleteOutput' in output` to distinguish an `OutputSpec` from a Zod schema. If the AI SDK renames that method, or a Zod schema happens to have a property named `parseCompleteOutput`, detection breaks silently.
468
+
469
+ ### Acceptance Criteria
470
+
471
+ - [ ] Use a more robust discriminant (e.g. `instanceof`, brand checking, or a symbol)
472
+ - [ ] OR document the fragility and add a test that catches regressions on AI SDK upgrades
473
+
474
+ </issue>
475
+
476
+ ---
477
+
478
+ <issue id="21" severity="medium" category="logic" status="closed">
479
+
480
+ ## #21 — Zod array element extraction uses private `_zod` internals
481
+
482
+ **File:** `src/core/agent/output.ts:43-50`
483
+
484
+ ### Description
485
+
486
+ The code accesses `(output as unknown as Record<string, unknown>)._zod` to extract the element schema from a Zod array. This relies on Zod's internal `_zod` property which can change across versions. If the structure changes, array output detection silently falls back to `Output.object({ schema: output })`, producing incorrect parsing.
487
+
488
+ ### Acceptance Criteria
489
+
490
+ - [ ] Use Zod's public API for array element introspection
491
+ - [ ] A test verifies correct array element extraction across the supported Zod version
492
+ - [ ] Add a regression test that catches breakage if Zod internals change
493
+
494
+ </issue>
495
+
496
+ ---
497
+
498
+ <issue id="22" severity="low" category="improvement" status="closed">
499
+
500
+ ## #22 — Orphaned AbortController in workflow `generate()`
501
+
502
+ **File:** `src/core/workflows/workflow.ts:368`
503
+
504
+ ### Description
505
+
506
+ ```typescript
507
+ const signal = overrides?.signal ?? new AbortController().signal;
508
+ ```
509
+
510
+ When no signal is provided, a new `AbortController` is created but its reference is immediately discarded. The signal can never fire. Harmless but wasteful.
511
+
512
+ ### Acceptance Criteria
513
+
514
+ - [ ] Only create an AbortController when needed, or pass `undefined` and check `signal?.aborted` in loops
515
+ - [ ] OR keep the current behavior and document it as intentional (a never-firing signal simplifies downstream code that always expects a signal)
516
+
517
+ </issue>
518
+
519
+ ---
520
+
521
+ <issue id="23" severity="low" category="improvement" status="closed">
522
+
523
+ ## #23 — O(N^2) array copying in `attemptEachAsync`
524
+
525
+ **File:** `src/utils/attempt.ts:34-36`
526
+
527
+ ### Description
528
+
529
+ ```typescript
530
+ async (acc, h) => [...(await acc), await attemptAsync<T, E>(async () => h())];
531
+ ```
532
+
533
+ Each reduce iteration creates a new array via spread. For N handlers, this is O(N^2) element copies. N is typically 1-2 so the impact is negligible, but the pattern is unnecessarily wasteful.
534
+
535
+ ### Acceptance Criteria
536
+
537
+ - [ ] Replace `reduce` with a simple `for` loop using `push`
538
+ - [ ] Behavior and return type remain identical
539
+
540
+ </issue>
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joggr, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ <div align="center">
2
+ <p><strong>@funkai/agents</strong></p>
3
+ <p>Lightweight workflow and agent orchestration framework built on the <a href="https://github.com/vercel/ai">Vercel AI SDK</a>.</p>
4
+
5
+ <a href="https://www.npmjs.com/package/@funkai/agents"><img src="https://img.shields.io/npm/v/@funkai/agents" alt="npm version" /></a>
6
+ <a href="https://github.com/joggrdocs/funkai/blob/main/LICENSE"><img src="https://img.shields.io/github/license/joggrdocs/funkai" alt="License" /></a>
7
+
8
+ </div>
9
+
10
+ ## Features
11
+
12
+ - :zap: **Functions all the way down** — `agent`, `tool`, `workflow` are functions that return plain objects.
13
+ - :jigsaw: **Composition over configuration** — Combine small pieces instead of configuring large ones.
14
+ - :shield: **Result, never throw** — Every public method returns `Result<T>`. Pattern-match on `ok` instead of try/catch.
15
+ - :lock: **Closures are state** — Workflow state is just `let` variables in your handler.
16
+ - :mag: **`$` is optional sugar** — The `$` helpers register data flow for observability; plain imperative code works too.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @funkai/agents
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Agent
27
+
28
+ ```ts
29
+ import { agent } from "@funkai/agents";
30
+
31
+ const helper = agent({
32
+ name: "helper",
33
+ model: "openai/gpt-4.1",
34
+ system: "You are a helpful assistant.",
35
+ });
36
+
37
+ const result = await helper.generate("What is TypeScript?");
38
+
39
+ if (!result.ok) {
40
+ console.error(result.error.code, result.error.message);
41
+ } else {
42
+ console.log(result.output);
43
+ }
44
+ ```
45
+
46
+ ### Tool
47
+
48
+ ```ts
49
+ import { tool } from "@funkai/agents";
50
+ import { z } from "zod";
51
+
52
+ const fetchPage = tool({
53
+ description: "Fetch the contents of a web page by URL",
54
+ inputSchema: z.object({ url: z.url() }),
55
+ execute: async ({ url }) => {
56
+ const res = await fetch(url);
57
+ return { url, status: res.status, body: await res.text() };
58
+ },
59
+ });
60
+ ```
61
+
62
+ ### Workflow
63
+
64
+ ```ts
65
+ import { workflow } from "@funkai/agents";
66
+ import { z } from "zod";
67
+
68
+ const research = workflow(
69
+ {
70
+ name: "research",
71
+ input: z.object({ topic: z.string() }),
72
+ output: z.object({ summary: z.string(), sources: z.array(z.string()) }),
73
+ },
74
+ async ({ input, $ }) => {
75
+ let sources: string[] = [];
76
+
77
+ const data = await $.step({
78
+ id: "fetch-sources",
79
+ execute: async () => findSources(input.topic),
80
+ });
81
+ if (data.ok) sources = data.value;
82
+
83
+ const analysis = await $.agent({
84
+ id: "summarize",
85
+ agent: summarizer,
86
+ input: { text: sources.join("\n") },
87
+ });
88
+
89
+ return {
90
+ summary: analysis.ok ? analysis.output : "Failed to summarize",
91
+ sources,
92
+ };
93
+ },
94
+ );
95
+
96
+ const result = await research.generate({ topic: "Effect systems" });
97
+ ```
98
+
99
+ ### Streaming
100
+
101
+ ```ts
102
+ const result = await helper.stream("Explain closures");
103
+
104
+ if (result.ok) {
105
+ for await (const chunk of result.stream) {
106
+ process.stdout.write(chunk);
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## API
112
+
113
+ | Export | Description |
114
+ | ------------------------------ | --------------------------------------------------------------------- |
115
+ | `agent(config)` | Create an agent. Returns `{ generate, stream, fn }`. |
116
+ | `tool(config)` | Create a tool for function calling. |
117
+ | `workflow(config, handler)` | Create a workflow with typed I/O and tracked steps. |
118
+ | `createWorkflowEngine(config)` | Create a workflow factory with shared configuration and custom steps. |
119
+ | `openrouter(modelId)` | Shorthand to create an OpenRouter language model from env key. |
120
+ | `createOpenRouter(options?)` | Create a reusable OpenRouter provider instance. |
121
+
122
+ ## Documentation
123
+
124
+ For comprehensive documentation, see [docs/overview.md](docs/overview.md).
125
+
126
+ ## License
127
+
128
+ [MIT](../../LICENSE)