@struktur/sdk 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 (96) hide show
  1. package/README.md +79 -0
  2. package/package.json +33 -0
  3. package/src/artifacts/AGENTS.md +16 -0
  4. package/src/artifacts/fileToArtifact.test.ts +37 -0
  5. package/src/artifacts/fileToArtifact.ts +44 -0
  6. package/src/artifacts/input.test.ts +243 -0
  7. package/src/artifacts/input.ts +360 -0
  8. package/src/artifacts/providers.test.ts +19 -0
  9. package/src/artifacts/providers.ts +7 -0
  10. package/src/artifacts/urlToArtifact.test.ts +23 -0
  11. package/src/artifacts/urlToArtifact.ts +19 -0
  12. package/src/auth/AGENTS.md +11 -0
  13. package/src/auth/config.test.ts +132 -0
  14. package/src/auth/config.ts +129 -0
  15. package/src/auth/tokens.test.ts +58 -0
  16. package/src/auth/tokens.ts +229 -0
  17. package/src/chunking/AGENTS.md +11 -0
  18. package/src/chunking/ArtifactBatcher.test.ts +22 -0
  19. package/src/chunking/ArtifactBatcher.ts +110 -0
  20. package/src/chunking/ArtifactSplitter.test.ts +38 -0
  21. package/src/chunking/ArtifactSplitter.ts +151 -0
  22. package/src/debug/AGENTS.md +79 -0
  23. package/src/debug/logger.test.ts +244 -0
  24. package/src/debug/logger.ts +211 -0
  25. package/src/extract.test.ts +22 -0
  26. package/src/extract.ts +114 -0
  27. package/src/fields.test.ts +663 -0
  28. package/src/fields.ts +239 -0
  29. package/src/index.test.ts +20 -0
  30. package/src/index.ts +93 -0
  31. package/src/llm/AGENTS.md +9 -0
  32. package/src/llm/LLMClient.test.ts +196 -0
  33. package/src/llm/LLMClient.ts +106 -0
  34. package/src/llm/RetryingRunner.test.ts +174 -0
  35. package/src/llm/RetryingRunner.ts +188 -0
  36. package/src/llm/message.test.ts +42 -0
  37. package/src/llm/message.ts +47 -0
  38. package/src/llm/models.test.ts +82 -0
  39. package/src/llm/models.ts +190 -0
  40. package/src/merge/AGENTS.md +6 -0
  41. package/src/merge/Deduplicator.test.ts +108 -0
  42. package/src/merge/Deduplicator.ts +45 -0
  43. package/src/merge/SmartDataMerger.test.ts +177 -0
  44. package/src/merge/SmartDataMerger.ts +56 -0
  45. package/src/parsers/AGENTS.md +58 -0
  46. package/src/parsers/collect.test.ts +56 -0
  47. package/src/parsers/collect.ts +31 -0
  48. package/src/parsers/index.ts +6 -0
  49. package/src/parsers/mime.test.ts +91 -0
  50. package/src/parsers/mime.ts +137 -0
  51. package/src/parsers/npm.ts +26 -0
  52. package/src/parsers/pdf.test.ts +394 -0
  53. package/src/parsers/pdf.ts +194 -0
  54. package/src/parsers/runner.test.ts +95 -0
  55. package/src/parsers/runner.ts +177 -0
  56. package/src/parsers/types.ts +29 -0
  57. package/src/prompts/AGENTS.md +8 -0
  58. package/src/prompts/DeduplicationPrompt.test.ts +41 -0
  59. package/src/prompts/DeduplicationPrompt.ts +37 -0
  60. package/src/prompts/ExtractorPrompt.test.ts +21 -0
  61. package/src/prompts/ExtractorPrompt.ts +72 -0
  62. package/src/prompts/ParallelMergerPrompt.test.ts +8 -0
  63. package/src/prompts/ParallelMergerPrompt.ts +37 -0
  64. package/src/prompts/SequentialExtractorPrompt.test.ts +24 -0
  65. package/src/prompts/SequentialExtractorPrompt.ts +82 -0
  66. package/src/prompts/formatArtifacts.test.ts +39 -0
  67. package/src/prompts/formatArtifacts.ts +46 -0
  68. package/src/strategies/AGENTS.md +6 -0
  69. package/src/strategies/DoublePassAutoMergeStrategy.test.ts +53 -0
  70. package/src/strategies/DoublePassAutoMergeStrategy.ts +270 -0
  71. package/src/strategies/DoublePassStrategy.test.ts +48 -0
  72. package/src/strategies/DoublePassStrategy.ts +179 -0
  73. package/src/strategies/ParallelAutoMergeStrategy.test.ts +152 -0
  74. package/src/strategies/ParallelAutoMergeStrategy.ts +241 -0
  75. package/src/strategies/ParallelStrategy.test.ts +61 -0
  76. package/src/strategies/ParallelStrategy.ts +157 -0
  77. package/src/strategies/SequentialAutoMergeStrategy.test.ts +66 -0
  78. package/src/strategies/SequentialAutoMergeStrategy.ts +222 -0
  79. package/src/strategies/SequentialStrategy.test.ts +53 -0
  80. package/src/strategies/SequentialStrategy.ts +119 -0
  81. package/src/strategies/SimpleStrategy.test.ts +46 -0
  82. package/src/strategies/SimpleStrategy.ts +74 -0
  83. package/src/strategies/concurrency.test.ts +16 -0
  84. package/src/strategies/concurrency.ts +14 -0
  85. package/src/strategies/index.test.ts +20 -0
  86. package/src/strategies/index.ts +7 -0
  87. package/src/strategies/utils.test.ts +76 -0
  88. package/src/strategies/utils.ts +56 -0
  89. package/src/tokenization.test.ts +119 -0
  90. package/src/tokenization.ts +71 -0
  91. package/src/types.test.ts +25 -0
  92. package/src/types.ts +116 -0
  93. package/src/validation/AGENTS.md +6 -0
  94. package/src/validation/validator.test.ts +172 -0
  95. package/src/validation/validator.ts +82 -0
  96. package/tsconfig.json +22 -0
@@ -0,0 +1,222 @@
1
+ import type { ExtractionResult, ExtractionStrategy } from "../types";
2
+ import type { ExtractionOptions } from "../types";
3
+ import { buildExtractorPrompt } from "../prompts/ExtractorPrompt";
4
+ import { buildDeduplicationPrompt } from "../prompts/DeduplicationPrompt";
5
+ import {
6
+ extractWithPrompt,
7
+ getBatches,
8
+ mergeUsage,
9
+ serializeSchema,
10
+ } from "./utils";
11
+ import { SmartDataMerger } from "../merge/SmartDataMerger";
12
+ import {
13
+ findExactDuplicatesWithHashing,
14
+ deduplicateByIndices,
15
+ } from "../merge/Deduplicator";
16
+ import { runWithRetries } from "../llm/RetryingRunner";
17
+
18
+ export type SequentialAutoMergeStrategyConfig = {
19
+ model: unknown;
20
+ chunkSize: number;
21
+ maxImages?: number;
22
+ outputInstructions?: string;
23
+ dedupeModel?: unknown;
24
+ execute?: typeof runWithRetries;
25
+ dedupeExecute?: typeof runWithRetries;
26
+ strict?: boolean;
27
+ };
28
+
29
+ const dedupeSchema = {
30
+ type: "object",
31
+ properties: {
32
+ keys: { type: "array", items: { type: "string" } },
33
+ },
34
+ required: ["keys"],
35
+ additionalProperties: false,
36
+ } as const;
37
+
38
+ const dedupeArrays = (data: Record<string, unknown>) => {
39
+ const result: Record<string, unknown> = { ...data };
40
+ for (const [key, value] of Object.entries(result)) {
41
+ if (Array.isArray(value)) {
42
+ const duplicates = findExactDuplicatesWithHashing(value);
43
+ result[key] = deduplicateByIndices(value, duplicates);
44
+ }
45
+ }
46
+ return result;
47
+ };
48
+
49
+ const removeByPath = (data: Record<string, unknown>, path: string) => {
50
+ const [root, indexStr] = path.split(".");
51
+ const index = Number(indexStr);
52
+ if (!root || Number.isNaN(index)) {
53
+ return data;
54
+ }
55
+
56
+ const value = data[root];
57
+ if (!Array.isArray(value)) {
58
+ return data;
59
+ }
60
+
61
+ const next = [...value];
62
+ next.splice(index, 1);
63
+ return { ...data, [root]: next };
64
+ };
65
+
66
+ export class SequentialAutoMergeStrategy<T> implements ExtractionStrategy<T> {
67
+ public name = "sequential-auto-merge";
68
+ private config: SequentialAutoMergeStrategyConfig;
69
+
70
+ constructor(config: SequentialAutoMergeStrategyConfig) {
71
+ this.config = config;
72
+ }
73
+
74
+ getEstimatedSteps(artifacts: ExtractionOptions<T>["artifacts"]): number {
75
+ const batches = getBatches(artifacts, {
76
+ maxTokens: this.config.chunkSize,
77
+ maxImages: this.config.maxImages,
78
+ });
79
+ return batches.length + 3;
80
+ }
81
+
82
+ async run(options: ExtractionOptions<T>): Promise<ExtractionResult<T>> {
83
+ const debug = options.debug;
84
+ const batches = getBatches(
85
+ options.artifacts,
86
+ {
87
+ maxTokens: this.config.chunkSize,
88
+ maxImages: this.config.maxImages,
89
+ },
90
+ debug,
91
+ );
92
+
93
+ const schema = serializeSchema(options.schema);
94
+ const merger = new SmartDataMerger(
95
+ options.schema as Record<string, unknown>,
96
+ );
97
+ let merged = {} as Record<string, unknown>;
98
+ const usages = [];
99
+ const totalSteps = this.getEstimatedSteps(options.artifacts);
100
+ let step = 1;
101
+
102
+ debug?.mergeStart({
103
+ mergeId: "sequential_auto_merge",
104
+ inputCount: batches.length,
105
+ strategy: this.name,
106
+ });
107
+
108
+ for (const [index, batch] of batches.entries()) {
109
+ const prompt = buildExtractorPrompt(
110
+ batch,
111
+ schema,
112
+ this.config.outputInstructions,
113
+ );
114
+ const result = await extractWithPrompt<T>({
115
+ model: this.config.model,
116
+ schema: options.schema,
117
+ system: prompt.system,
118
+ user: prompt.user,
119
+ artifacts: batch,
120
+ events: options.events,
121
+ execute: this.config.execute as never,
122
+ strict: options.strict ?? this.config.strict,
123
+ debug,
124
+ callId: `sequential_auto_batch_${index + 1}`,
125
+ });
126
+
127
+ merged = merger.merge(merged, result.data as Record<string, unknown>);
128
+ usages.push(result.usage);
129
+
130
+ // Log merge operation per field
131
+ for (const key of Object.keys(result.data as Record<string, unknown>)) {
132
+ const leftArray = Array.isArray(merged[key])
133
+ ? (merged[key] as unknown[]).length
134
+ : undefined;
135
+ const rightArray = Array.isArray(
136
+ (result.data as Record<string, unknown>)[key],
137
+ )
138
+ ? ((result.data as Record<string, unknown>)[key] as unknown[]).length
139
+ : undefined;
140
+
141
+ debug?.smartMergeField({
142
+ mergeId: "sequential_auto_merge",
143
+ field: key,
144
+ operation: "merge_arrays",
145
+ leftCount: leftArray,
146
+ rightCount: rightArray,
147
+ });
148
+ }
149
+
150
+ step += 1;
151
+ await options.events?.onStep?.({
152
+ step,
153
+ total: totalSteps,
154
+ label: `batch ${index + 1}/${batches.length}`,
155
+ });
156
+ debug?.step({
157
+ step,
158
+ total: totalSteps,
159
+ label: `batch ${index + 1}/${batches.length}`,
160
+ strategy: this.name,
161
+ });
162
+ }
163
+
164
+ debug?.mergeComplete({ mergeId: "sequential_auto_merge", success: true });
165
+
166
+ merged = dedupeArrays(merged);
167
+
168
+ const dedupePrompt = buildDeduplicationPrompt(schema, merged);
169
+
170
+ debug?.dedupeStart({
171
+ dedupeId: "sequential_auto_dedupe",
172
+ itemCount: Object.keys(merged).length,
173
+ });
174
+
175
+ const dedupeResponse = await runWithRetries<{ keys: string[] }>({
176
+ model: this.config.dedupeModel ?? this.config.model,
177
+ schema: dedupeSchema,
178
+ system: dedupePrompt.system,
179
+ user: dedupePrompt.user,
180
+ events: options.events,
181
+ execute: this.config.dedupeExecute,
182
+ strict: this.config.strict,
183
+ debug,
184
+ callId: "sequential_auto_dedupe",
185
+ });
186
+
187
+ step += 1;
188
+ await options.events?.onStep?.({
189
+ step,
190
+ total: totalSteps,
191
+ label: "dedupe",
192
+ });
193
+ debug?.step({
194
+ step,
195
+ total: totalSteps,
196
+ label: "dedupe",
197
+ strategy: this.name,
198
+ });
199
+
200
+ let deduped = merged;
201
+ for (const key of dedupeResponse.data.keys) {
202
+ deduped = removeByPath(deduped, key);
203
+ }
204
+
205
+ debug?.dedupeComplete({
206
+ dedupeId: "sequential_auto_dedupe",
207
+ duplicatesFound: dedupeResponse.data.keys.length,
208
+ itemsRemoved: dedupeResponse.data.keys.length,
209
+ });
210
+
211
+ return {
212
+ data: deduped as T,
213
+ usage: mergeUsage([...usages, dedupeResponse.usage]),
214
+ };
215
+ }
216
+ }
217
+
218
+ export const sequentialAutoMerge = <T>(
219
+ config: SequentialAutoMergeStrategyConfig,
220
+ ) => {
221
+ return new SequentialAutoMergeStrategy<T>(config);
222
+ };
@@ -0,0 +1,53 @@
1
+ import { test, expect } from "bun:test";
2
+ import type { JSONSchemaType } from "ajv";
3
+ import { SequentialStrategy } from "./SequentialStrategy";
4
+ import type { Artifact, ExtractionOptions } from "../types";
5
+
6
+ type Output = { title: string };
7
+
8
+ const schema: JSONSchemaType<Output> = {
9
+ type: "object",
10
+ properties: { title: { type: "string" } },
11
+ required: ["title"],
12
+ additionalProperties: false,
13
+ };
14
+
15
+ const artifacts: Artifact[] = [
16
+ {
17
+ id: "a1",
18
+ type: "text",
19
+ raw: async () => Buffer.from(""),
20
+ contents: [{ text: "abcdefgh" }],
21
+ },
22
+ {
23
+ id: "a2",
24
+ type: "text",
25
+ raw: async () => Buffer.from(""),
26
+ contents: [{ text: "abcdefgh" }],
27
+ },
28
+ ];
29
+
30
+ test("SequentialStrategy processes batches in order", async () => {
31
+ let calls = 0;
32
+ const strategy = new SequentialStrategy<Output>({
33
+ model: {},
34
+ chunkSize: 2,
35
+ execute: (async () => {
36
+ calls += 1;
37
+ return {
38
+ data: { title: `step-${calls}` },
39
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
40
+ };
41
+ }) as any,
42
+ });
43
+
44
+ const options: ExtractionOptions<Output> = {
45
+ artifacts,
46
+ schema,
47
+ strategy,
48
+ };
49
+
50
+ const result = await strategy.run(options);
51
+ expect(result.data.title).toBe("step-2");
52
+ expect(calls).toBe(2);
53
+ });
@@ -0,0 +1,119 @@
1
+ import type { ExtractionResult, ExtractionStrategy } from "../types";
2
+ import type { ExtractionOptions } from "../types";
3
+ import { buildSequentialPrompt } from "../prompts/SequentialExtractorPrompt";
4
+ import {
5
+ extractWithPrompt,
6
+ getBatches,
7
+ mergeUsage,
8
+ serializeSchema,
9
+ } from "./utils";
10
+ import { runWithRetries } from "../llm/RetryingRunner";
11
+
12
+ export type SequentialStrategyConfig = {
13
+ model: unknown;
14
+ chunkSize: number;
15
+ maxImages?: number;
16
+ outputInstructions?: string;
17
+ execute?: typeof runWithRetries;
18
+ strict?: boolean;
19
+ };
20
+
21
+ export class SequentialStrategy<T> implements ExtractionStrategy<T> {
22
+ public name = "sequential";
23
+ private config: SequentialStrategyConfig;
24
+
25
+ constructor(config: SequentialStrategyConfig) {
26
+ this.config = config;
27
+ }
28
+
29
+ getEstimatedSteps(artifacts: ExtractionOptions<T>["artifacts"]): number {
30
+ const batches = getBatches(artifacts, {
31
+ maxTokens: this.config.chunkSize,
32
+ maxImages: this.config.maxImages,
33
+ });
34
+ return batches.length + 2;
35
+ }
36
+
37
+ async run(options: ExtractionOptions<T>): Promise<ExtractionResult<T>> {
38
+ const debug = options.debug;
39
+ const batches = getBatches(
40
+ options.artifacts,
41
+ {
42
+ maxTokens: this.config.chunkSize,
43
+ maxImages: this.config.maxImages,
44
+ },
45
+ debug,
46
+ );
47
+
48
+ const schema = serializeSchema(options.schema);
49
+ let currentData: T | undefined;
50
+ const usages = [];
51
+ const totalSteps = this.getEstimatedSteps(options.artifacts);
52
+ let step = 1;
53
+
54
+ // Emit start event
55
+ await options.events?.onStep?.({
56
+ step,
57
+ total: totalSteps,
58
+ label: batches.length > 1 ? `batch 1/${batches.length}` : "extract",
59
+ });
60
+ debug?.step({
61
+ step,
62
+ total: totalSteps,
63
+ label: batches.length > 1 ? `batch 1/${batches.length}` : "extract",
64
+ strategy: this.name,
65
+ });
66
+
67
+ for (const [index, batch] of batches.entries()) {
68
+ const previousData = currentData ? JSON.stringify(currentData) : "{}";
69
+ const prompt = buildSequentialPrompt(
70
+ batch,
71
+ schema,
72
+ previousData,
73
+ this.config.outputInstructions,
74
+ );
75
+
76
+ const result = await extractWithPrompt<T>({
77
+ model: this.config.model,
78
+ schema: options.schema,
79
+ system: prompt.system,
80
+ user: prompt.user,
81
+ artifacts: batch,
82
+ events: options.events,
83
+ execute: this.config.execute as never,
84
+ strict: options.strict ?? this.config.strict,
85
+ debug,
86
+ callId: `sequential_batch_${index + 1}`,
87
+ });
88
+
89
+ currentData = result.data;
90
+ usages.push(result.usage);
91
+
92
+ step += 1;
93
+ // Only emit progress if there are more batches
94
+ if (index < batches.length - 1) {
95
+ await options.events?.onStep?.({
96
+ step,
97
+ total: totalSteps,
98
+ label: `batch ${index + 2}/${batches.length}`,
99
+ });
100
+ debug?.step({
101
+ step,
102
+ total: totalSteps,
103
+ label: `batch ${index + 2}/${batches.length}`,
104
+ strategy: this.name,
105
+ });
106
+ }
107
+ }
108
+
109
+ if (!currentData) {
110
+ throw new Error("No data extracted from sequential strategy");
111
+ }
112
+
113
+ return { data: currentData, usage: mergeUsage(usages) };
114
+ }
115
+ }
116
+
117
+ export const sequential = <T>(config: SequentialStrategyConfig) => {
118
+ return new SequentialStrategy<T>(config);
119
+ };
@@ -0,0 +1,46 @@
1
+ import { test, expect } from "bun:test";
2
+ import type { JSONSchemaType } from "ajv";
3
+ import { SimpleStrategy } from "./SimpleStrategy";
4
+ import type { Artifact, ExtractionOptions } from "../types";
5
+
6
+ type Output = { title: string };
7
+
8
+ const schema: JSONSchemaType<Output> = {
9
+ type: "object",
10
+ properties: { title: { type: "string" } },
11
+ required: ["title"],
12
+ additionalProperties: false,
13
+ };
14
+
15
+ const artifacts: Artifact[] = [
16
+ {
17
+ id: "a1",
18
+ type: "text",
19
+ raw: async () => Buffer.from(""),
20
+ contents: [{ text: "hello" }],
21
+ },
22
+ ];
23
+
24
+ test("SimpleStrategy runs once", async () => {
25
+ let calls = 0;
26
+ const strategy = new SimpleStrategy<Output>({
27
+ model: {},
28
+ execute: (async () => {
29
+ calls += 1;
30
+ return {
31
+ data: { title: "ok" },
32
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
33
+ };
34
+ }) as any,
35
+ });
36
+
37
+ const options: ExtractionOptions<Output> = {
38
+ artifacts,
39
+ schema,
40
+ strategy,
41
+ };
42
+
43
+ const result = await strategy.run(options);
44
+ expect(result.data.title).toBe("ok");
45
+ expect(calls).toBe(1);
46
+ });
@@ -0,0 +1,74 @@
1
+ import type { ExtractionResult, ExtractionStrategy } from "../types";
2
+ import type { ExtractionOptions } from "../types";
3
+ import { buildExtractorPrompt } from "../prompts/ExtractorPrompt";
4
+ import { extractWithPrompt, serializeSchema } from "./utils";
5
+ import { runWithRetries } from "../llm/RetryingRunner";
6
+
7
+ export type SimpleStrategyConfig = {
8
+ model: unknown;
9
+ outputInstructions?: string;
10
+ execute?: typeof runWithRetries;
11
+ strict?: boolean;
12
+ };
13
+
14
+ export class SimpleStrategy<T> implements ExtractionStrategy<T> {
15
+ public name = "simple";
16
+ private config: SimpleStrategyConfig;
17
+
18
+ constructor(config: SimpleStrategyConfig) {
19
+ this.config = config;
20
+ }
21
+
22
+ getEstimatedSteps(): number {
23
+ return 3;
24
+ }
25
+
26
+ async run(options: ExtractionOptions<T>): Promise<ExtractionResult<T>> {
27
+ const debug = options.debug;
28
+ const schema = serializeSchema(options.schema);
29
+ const { system, user } = buildExtractorPrompt(
30
+ options.artifacts,
31
+ schema,
32
+ this.config.outputInstructions,
33
+ );
34
+
35
+ // Emit start event before extraction begins
36
+ await options.events?.onStep?.({
37
+ step: 1,
38
+ total: this.getEstimatedSteps(),
39
+ label: "extract",
40
+ });
41
+ debug?.step({
42
+ step: 1,
43
+ total: this.getEstimatedSteps(),
44
+ label: "extract",
45
+ strategy: this.name,
46
+ });
47
+
48
+ const result = await extractWithPrompt<T>({
49
+ model: this.config.model,
50
+ schema: options.schema,
51
+ system,
52
+ user,
53
+ artifacts: options.artifacts,
54
+ events: options.events,
55
+ execute: this.config.execute as never,
56
+ strict: options.strict ?? this.config.strict,
57
+ debug,
58
+ callId: "simple_extract",
59
+ });
60
+
61
+ debug?.step({
62
+ step: 2,
63
+ total: this.getEstimatedSteps(),
64
+ label: "complete",
65
+ strategy: this.name,
66
+ });
67
+
68
+ return { data: result.data, usage: result.usage };
69
+ }
70
+ }
71
+
72
+ export const simple = <T>(config: SimpleStrategyConfig) => {
73
+ return new SimpleStrategy<T>(config);
74
+ };
@@ -0,0 +1,16 @@
1
+ import { test, expect } from "bun:test";
2
+ import { runConcurrently } from "./concurrency";
3
+
4
+ test("runConcurrently runs tasks in batches", async () => {
5
+ const started: number[] = [];
6
+ const tasks = [1, 2, 3, 4, 5].map((value) => async () => {
7
+ started.push(value);
8
+ await new Promise((resolve) => setTimeout(resolve, 5));
9
+ return value;
10
+ });
11
+
12
+ const results = await runConcurrently(tasks, 2);
13
+
14
+ expect(results).toEqual([1, 2, 3, 4, 5]);
15
+ expect(started).toEqual([1, 2, 3, 4, 5]);
16
+ });
@@ -0,0 +1,14 @@
1
+ export const runConcurrently = async <T>(
2
+ tasks: Array<() => Promise<T>>,
3
+ concurrency: number
4
+ ): Promise<T[]> => {
5
+ const results: T[] = [];
6
+
7
+ for (let i = 0; i < tasks.length; i += concurrency) {
8
+ const chunk = tasks.slice(i, i + concurrency).map((task) => task());
9
+ const chunkResults = await Promise.all(chunk);
10
+ results.push(...chunkResults);
11
+ }
12
+
13
+ return results;
14
+ };
@@ -0,0 +1,20 @@
1
+ import { test, expect } from "bun:test";
2
+ import * as strategies from "./index";
3
+
4
+ test("strategies index re-exports constructors and helpers", () => {
5
+ expect(typeof strategies.SimpleStrategy).toBe("function");
6
+ expect(typeof strategies.ParallelStrategy).toBe("function");
7
+ expect(typeof strategies.SequentialStrategy).toBe("function");
8
+ expect(typeof strategies.ParallelAutoMergeStrategy).toBe("function");
9
+ expect(typeof strategies.SequentialAutoMergeStrategy).toBe("function");
10
+ expect(typeof strategies.DoublePassStrategy).toBe("function");
11
+ expect(typeof strategies.DoublePassAutoMergeStrategy).toBe("function");
12
+
13
+ expect(typeof strategies.simple).toBe("function");
14
+ expect(typeof strategies.parallel).toBe("function");
15
+ expect(typeof strategies.sequential).toBe("function");
16
+ expect(typeof strategies.parallelAutoMerge).toBe("function");
17
+ expect(typeof strategies.sequentialAutoMerge).toBe("function");
18
+ expect(typeof strategies.doublePass).toBe("function");
19
+ expect(typeof strategies.doublePassAutoMerge).toBe("function");
20
+ });
@@ -0,0 +1,7 @@
1
+ export { SimpleStrategy, simple } from "./SimpleStrategy";
2
+ export { ParallelStrategy, parallel } from "./ParallelStrategy";
3
+ export { SequentialStrategy, sequential } from "./SequentialStrategy";
4
+ export { ParallelAutoMergeStrategy, parallelAutoMerge } from "./ParallelAutoMergeStrategy";
5
+ export { SequentialAutoMergeStrategy, sequentialAutoMerge } from "./SequentialAutoMergeStrategy";
6
+ export { DoublePassStrategy, doublePass } from "./DoublePassStrategy";
7
+ export { DoublePassAutoMergeStrategy, doublePassAutoMerge } from "./DoublePassAutoMergeStrategy";
@@ -0,0 +1,76 @@
1
+ import { test, expect } from "bun:test";
2
+ import type { Artifact } from "../types";
3
+ import type { JSONSchemaType } from "ajv";
4
+ import { batchArtifacts } from "../chunking/ArtifactBatcher";
5
+ import { serializeSchema, mergeUsage, getBatches, extractWithPrompt } from "./utils";
6
+
7
+ type Output = { title: string };
8
+
9
+ const schema: JSONSchemaType<Output> = {
10
+ type: "object",
11
+ properties: { title: { type: "string" } },
12
+ required: ["title"],
13
+ additionalProperties: false,
14
+ };
15
+
16
+ const makeArtifact = (id: string, text: string): Artifact => ({
17
+ id,
18
+ type: "text",
19
+ raw: async () => Buffer.from(text),
20
+ contents: [{ text }],
21
+ });
22
+
23
+ test("serializeSchema returns JSON", () => {
24
+ expect(serializeSchema({ ok: true })).toBe("{\"ok\":true}");
25
+ });
26
+
27
+ test("mergeUsage sums token usage", () => {
28
+ const usage = mergeUsage([
29
+ { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
30
+ { inputTokens: 4, outputTokens: 5, totalTokens: 9 },
31
+ ]);
32
+
33
+ expect(usage).toEqual({ inputTokens: 5, outputTokens: 7, totalTokens: 12 });
34
+ });
35
+
36
+ test("getBatches delegates to batchArtifacts", () => {
37
+ const artifacts = [makeArtifact("a1", "hello"), makeArtifact("a2", "world")];
38
+ const options = { maxTokens: 1 };
39
+
40
+ expect(getBatches(artifacts, options)).toEqual(batchArtifacts(artifacts, options));
41
+ });
42
+
43
+ test("extractWithPrompt builds user content and returns result", async () => {
44
+ const artifacts: Artifact[] = [
45
+ {
46
+ id: "a1",
47
+ type: "image",
48
+ raw: async () => Buffer.from(""),
49
+ contents: [
50
+ {
51
+ text: "hello",
52
+ media: [{ type: "image", base64: "abc" }],
53
+ },
54
+ ],
55
+ },
56
+ ];
57
+
58
+ let receivedUser: unknown;
59
+ const result = await extractWithPrompt<Output>({
60
+ model: {},
61
+ schema,
62
+ system: "sys",
63
+ user: "prompt",
64
+ artifacts,
65
+ execute: async (request) => {
66
+ receivedUser = request.user;
67
+ return {
68
+ data: { title: "ok" },
69
+ usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
70
+ };
71
+ },
72
+ });
73
+
74
+ expect(Array.isArray(receivedUser)).toBe(true);
75
+ expect(result.data.title).toBe("ok");
76
+ });