@yolk-sdk/knowledge 0.1.0-canary.13 → 0.1.0-canary.130

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 (97) hide show
  1. package/README.md +160 -80
  2. package/dist/agent.d.mts +118 -19
  3. package/dist/agent.d.mts.map +1 -1
  4. package/dist/agent.mjs +256 -34
  5. package/dist/agent.mjs.map +1 -1
  6. package/dist/chunking.d.mts +13 -16
  7. package/dist/chunking.d.mts.map +1 -1
  8. package/dist/chunking.mjs +4 -4
  9. package/dist/chunking.mjs.map +1 -1
  10. package/dist/context.d.mts +4 -13
  11. package/dist/context.d.mts.map +1 -1
  12. package/dist/context.mjs +17 -13
  13. package/dist/context.mjs.map +1 -1
  14. package/dist/documents.d.mts +128 -78
  15. package/dist/documents.d.mts.map +1 -1
  16. package/dist/documents.mjs +89 -61
  17. package/dist/documents.mjs.map +1 -1
  18. package/dist/embeddings.d.mts +4 -7
  19. package/dist/embeddings.d.mts.map +1 -1
  20. package/dist/embeddings.mjs +1 -2
  21. package/dist/embeddings.mjs.map +1 -1
  22. package/dist/errors.d.mts +19 -22
  23. package/dist/errors.d.mts.map +1 -1
  24. package/dist/errors.mjs +12 -13
  25. package/dist/errors.mjs.map +1 -1
  26. package/dist/extraction.d.mts +4 -7
  27. package/dist/extraction.d.mts.map +1 -1
  28. package/dist/extraction.mjs +1 -2
  29. package/dist/extraction.mjs.map +1 -1
  30. package/dist/files.d.mts +19 -0
  31. package/dist/files.d.mts.map +1 -0
  32. package/dist/files.mjs +6 -0
  33. package/dist/files.mjs.map +1 -0
  34. package/dist/index.d.mts +4 -4
  35. package/dist/index.mjs +4 -4
  36. package/dist/ingestion.d.mts +21 -23
  37. package/dist/ingestion.d.mts.map +1 -1
  38. package/dist/ingestion.mjs +12 -17
  39. package/dist/ingestion.mjs.map +1 -1
  40. package/dist/search.d.mts +13 -17
  41. package/dist/search.d.mts.map +1 -1
  42. package/dist/search.mjs +9 -12
  43. package/dist/search.mjs.map +1 -1
  44. package/dist/store.d.mts +92 -29
  45. package/dist/store.d.mts.map +1 -1
  46. package/dist/store.mjs +3 -3
  47. package/dist/store.mjs.map +1 -1
  48. package/dist/summarization.d.mts +6 -9
  49. package/dist/summarization.d.mts.map +1 -1
  50. package/dist/summarization.mjs +1 -2
  51. package/dist/summarization.mjs.map +1 -1
  52. package/package.json +11 -41
  53. package/src/agent.ts +393 -65
  54. package/src/chunking.ts +27 -8
  55. package/src/context.ts +21 -16
  56. package/src/documents.ts +140 -71
  57. package/src/errors.ts +4 -4
  58. package/src/extraction.ts +4 -3
  59. package/src/files.ts +24 -0
  60. package/src/index.ts +6 -3
  61. package/src/ingestion.ts +46 -27
  62. package/src/search.ts +90 -62
  63. package/src/store.ts +139 -25
  64. package/src/summarization.ts +4 -3
  65. package/dist/artifacts.d.mts +0 -37
  66. package/dist/artifacts.d.mts.map +0 -1
  67. package/dist/artifacts.mjs +0 -28
  68. package/dist/artifacts.mjs.map +0 -1
  69. package/dist/links.d.mts +0 -17
  70. package/dist/links.d.mts.map +0 -1
  71. package/dist/links.mjs +0 -24
  72. package/dist/links.mjs.map +0 -1
  73. package/dist/provenance.d.mts +0 -20
  74. package/dist/provenance.d.mts.map +0 -1
  75. package/dist/provenance.mjs +0 -26
  76. package/dist/provenance.mjs.map +0 -1
  77. package/dist/records.d.mts +0 -59
  78. package/dist/records.d.mts.map +0 -1
  79. package/dist/records.mjs +0 -64
  80. package/dist/records.mjs.map +0 -1
  81. package/dist/representations.d.mts +0 -36
  82. package/dist/representations.d.mts.map +0 -1
  83. package/dist/representations.mjs +0 -44
  84. package/dist/representations.mjs.map +0 -1
  85. package/dist/search-store.d.mts +0 -77
  86. package/dist/search-store.d.mts.map +0 -1
  87. package/dist/search-store.mjs +0 -7
  88. package/dist/search-store.mjs.map +0 -1
  89. package/dist/vector-store.d.mts +0 -3
  90. package/dist/vector-store.mjs +0 -2
  91. package/src/artifacts.ts +0 -48
  92. package/src/links.ts +0 -23
  93. package/src/provenance.ts +0 -25
  94. package/src/records.ts +0 -76
  95. package/src/representations.ts +0 -51
  96. package/src/search-store.ts +0 -98
  97. package/src/vector-store.ts +0 -6
package/src/agent.ts CHANGED
@@ -2,95 +2,423 @@ import { Effect } from 'effect'
2
2
  import * as Schema from 'effect/Schema'
3
3
  import { ToolError } from '@yolk-sdk/agent/loop'
4
4
  import { ToolResult } from '@yolk-sdk/agent/protocol'
5
- import { makeTool, type ToolRegistration } from '@yolk-sdk/agent/tools'
6
- import type { KnowledgeScope } from './records.ts'
7
- import type { KnowledgeSearchScope } from './documents.ts'
8
- import type { KnowledgeSearcher } from './search.ts'
9
- import { packKnowledgeSearchContext } from './search.ts'
5
+ import { makeTool, withToolArgumentsErrorHint, type ToolRegistration } from '@yolk-sdk/agent/tools'
6
+ import {
7
+ KnowledgeAvailabilitySchema,
8
+ KnowledgeDocumentSchema,
9
+ NonNegativeInteger,
10
+ NonEmptyTrimmedString,
11
+ PositiveInteger,
12
+ type KnowledgeAvailability,
13
+ type KnowledgeDocument
14
+ } from './documents.ts'
10
15
 
11
- export type ResolveKnowledgeScope<Context> = (context: Context) => KnowledgeScope
16
+ const OptionalNonEmptyTrimmedString = Schema.optional(Schema.NullOr(NonEmptyTrimmedString))
12
17
 
13
- export type KnowledgeAgentContextOptions = {
14
- readonly maxPinnedContextCharacters: number
18
+ const OptionalPositiveInteger = Schema.optional(Schema.NullOr(PositiveInteger))
19
+
20
+ const OptionalNonNegativeInteger = Schema.optional(Schema.NullOr(NonNegativeInteger))
21
+
22
+ const OptionalNumber = Schema.optional(Schema.NullOr(Schema.Number))
23
+
24
+ const KnowledgeTargetParamsSchema = Schema.Struct({
25
+ slug: NonEmptyTrimmedString,
26
+ scope: OptionalNonEmptyTrimmedString
27
+ })
28
+
29
+ const KnowledgeLookupParams = Schema.Union([
30
+ Schema.Struct({
31
+ operation: Schema.Literal('search'),
32
+ query: NonEmptyTrimmedString,
33
+ limit: OptionalPositiveInteger,
34
+ minScore: OptionalNumber,
35
+ contextChunks: OptionalNonNegativeInteger
36
+ }),
37
+ Schema.Struct({
38
+ operation: Schema.Literal('get'),
39
+ id: OptionalNonEmptyTrimmedString,
40
+ target: Schema.optional(Schema.NullOr(KnowledgeTargetParamsSchema))
41
+ })
42
+ ])
43
+
44
+ const KnowledgeManageParams = Schema.Union([
45
+ Schema.Struct({
46
+ operation: Schema.Literal('upsert'),
47
+ target: KnowledgeTargetParamsSchema,
48
+ title: NonEmptyTrimmedString,
49
+ purpose: NonEmptyTrimmedString,
50
+ origin: NonEmptyTrimmedString,
51
+ content: NonEmptyTrimmedString,
52
+ availability: KnowledgeAvailabilitySchema
53
+ }),
54
+ Schema.Struct({
55
+ operation: Schema.Literal('set_availability'),
56
+ target: KnowledgeTargetParamsSchema,
57
+ availability: KnowledgeAvailabilitySchema
58
+ }),
59
+ Schema.Struct({
60
+ operation: Schema.Literal('rename_slug'),
61
+ target: KnowledgeTargetParamsSchema,
62
+ nextSlug: NonEmptyTrimmedString
63
+ }),
64
+ Schema.Struct({
65
+ operation: Schema.Literal('delete'),
66
+ target: KnowledgeTargetParamsSchema
67
+ })
68
+ ])
69
+
70
+ const KnowledgeLookupResultSchema = Schema.Struct({
71
+ document: KnowledgeDocumentSchema,
72
+ score: Schema.optionalKey(Schema.Number),
73
+ context: Schema.optionalKey(Schema.Array(Schema.Struct({ content: Schema.String })))
74
+ })
75
+
76
+ /** Declared output of `makeKnowledgeLookupTool`; `structuredContent` is its JSON encoding
77
+ * (`Schema.toCodecJson`: document dates become ISO strings). */
78
+ export const KnowledgeLookupOutput = Schema.Union([
79
+ Schema.Struct({
80
+ operation: Schema.Literal('search'),
81
+ results: Schema.Array(KnowledgeLookupResultSchema)
82
+ }),
83
+ Schema.Struct({
84
+ operation: Schema.Literal('get'),
85
+ document: KnowledgeDocumentSchema
86
+ })
87
+ ])
88
+
89
+ export type KnowledgeLookupOutput = typeof KnowledgeLookupOutput.Type
90
+
91
+ /** Declared output of `makeKnowledgeManageTool`; `structuredContent` is its JSON encoding. */
92
+ export const KnowledgeManageOutput = Schema.Struct({
93
+ operation: Schema.Literals(['upsert', 'set_availability', 'rename_slug', 'delete']),
94
+ document: Schema.Struct({ id: Schema.String, slug: Schema.String, title: Schema.String })
95
+ })
96
+
97
+ export type KnowledgeManageOutput = typeof KnowledgeManageOutput.Type
98
+
99
+ const encodeLookupOutput = Schema.encodeEffect(Schema.toCodecJson(KnowledgeLookupOutput))
100
+
101
+ const encodeManageOutput = Schema.encodeEffect(Schema.toCodecJson(KnowledgeManageOutput))
102
+
103
+ /** A handler result that does not encode to the declared output is a host bug, not a model
104
+ * mistake: fail with a safe message (no values). */
105
+ const outputError = (tool: string) => () =>
106
+ new ToolError({
107
+ tool,
108
+ message: `${tool} produced a result that does not match its output schema.`,
109
+ cause: 'execution'
110
+ })
111
+
112
+ type KnowledgeTargetParams = typeof KnowledgeTargetParamsSchema.Type
113
+
114
+ type KnowledgeLookupParams = typeof KnowledgeLookupParams.Type
115
+
116
+ type KnowledgeManageParams = typeof KnowledgeManageParams.Type
117
+
118
+ export type KnowledgeTarget = {
119
+ readonly slug: string
120
+ readonly scope?: string
15
121
  }
16
122
 
17
- export const defaultKnowledgeAgentContextOptions: KnowledgeAgentContextOptions = {
18
- maxPinnedContextCharacters: 6000
123
+ export type KnowledgeLookupResult = {
124
+ readonly document: KnowledgeDocument
125
+ readonly score?: number
126
+ readonly context?: ReadonlyArray<{ readonly content: string }>
19
127
  }
20
128
 
21
- const KnowledgeSearchToolParams = Schema.Struct({
22
- query: Schema.Trimmed.pipe(
23
- Schema.check(Schema.isNonEmpty()),
24
- Schema.annotate({ description: 'Search query for the configured knowledge search.' })
25
- )
26
- })
129
+ export type KnowledgeSavedDocument = {
130
+ readonly id: string
131
+ readonly slug: string
132
+ readonly title: string
133
+ }
134
+
135
+ export type KnowledgeLookupHandlers<Context> = {
136
+ readonly search: (input: {
137
+ readonly context: Context
138
+ readonly query: string
139
+ readonly limit?: number
140
+ readonly minScore?: number
141
+ readonly contextChunks?: number
142
+ }) => Effect.Effect<ReadonlyArray<KnowledgeLookupResult>, ToolError>
143
+ readonly get: (input: {
144
+ readonly context: Context
145
+ readonly id?: string
146
+ readonly target?: KnowledgeTarget
147
+ }) => Effect.Effect<KnowledgeDocument, ToolError>
148
+ }
149
+
150
+ export type KnowledgeManageHandlers<Context> = {
151
+ readonly upsert: (input: {
152
+ readonly context: Context
153
+ readonly target: KnowledgeTarget
154
+ readonly title: string
155
+ readonly purpose: string
156
+ readonly origin: string
157
+ readonly content: string
158
+ readonly availability: KnowledgeAvailability
159
+ }) => Effect.Effect<KnowledgeSavedDocument, ToolError>
160
+ readonly setAvailability: (input: {
161
+ readonly context: Context
162
+ readonly target: KnowledgeTarget
163
+ readonly availability: KnowledgeAvailability
164
+ }) => Effect.Effect<KnowledgeSavedDocument, ToolError>
165
+ readonly renameSlug: (input: {
166
+ readonly context: Context
167
+ readonly target: KnowledgeTarget
168
+ readonly nextSlug: string
169
+ }) => Effect.Effect<KnowledgeSavedDocument, ToolError>
170
+ readonly delete: (input: {
171
+ readonly context: Context
172
+ readonly target: KnowledgeTarget
173
+ }) => Effect.Effect<KnowledgeSavedDocument, ToolError>
174
+ }
175
+
176
+ const optionalValue = <Value>(value: Value | null | undefined) => value ?? undefined
177
+
178
+ const targetFromParams = (target: KnowledgeTargetParams): KnowledgeTarget =>
179
+ target.scope === null || target.scope === undefined
180
+ ? { slug: target.slug }
181
+ : { slug: target.slug, scope: target.scope }
27
182
 
28
- const isToolError = Schema.is(ToolError)
183
+ const optionalTargetFromParams = (target: KnowledgeTargetParams | null | undefined) =>
184
+ target === null || target === undefined ? undefined : targetFromParams(target)
29
185
 
30
- export type KnowledgeSearchToolScopeResolver<Context> =
31
- | KnowledgeSearchScope
32
- | ((context: Context) => Effect.Effect<KnowledgeSearchScope, ToolError>)
186
+ const formatDocument = (document: KnowledgeDocument) =>
187
+ [
188
+ `# ${document.title}`,
189
+ `document_id: ${document.id}`,
190
+ `slug: ${document.slug}`,
191
+ `purpose: ${document.purpose}`,
192
+ `origin: ${document.origin}`,
193
+ `availability: ${document.availability}`,
194
+ `status: ${document.status}`,
195
+ '',
196
+ document.content
197
+ ].join('\n')
33
198
 
34
- export type MakeKnowledgeSearchToolOptions<Context> = {
35
- readonly scope: KnowledgeSearchToolScopeResolver<Context>
36
- readonly name?: string
37
- readonly description?: string
38
- readonly limit?: number
39
- readonly minScore?: number
40
- readonly contextChunks?: number
199
+ const formatSearchResults = (results: ReadonlyArray<KnowledgeLookupResult>) =>
200
+ results.length === 0
201
+ ? 'No knowledge matches found.'
202
+ : results
203
+ .map(result =>
204
+ [
205
+ `## ${result.document.title}`,
206
+ `document_id: ${result.document.id}`,
207
+ `slug: ${result.document.slug}`,
208
+ `purpose: ${result.document.purpose}`,
209
+ `origin: ${result.document.origin}`,
210
+ `availability: ${result.document.availability}`,
211
+ result.score === undefined ? undefined : `score: ${result.score}`,
212
+ '',
213
+ result.context?.map(chunk => chunk.content).join('\n\n') ?? result.document.content
214
+ ]
215
+ .filter(line => line !== undefined)
216
+ .join('\n')
217
+ )
218
+ .join('\n\n')
219
+
220
+ const formatSaved = (verb: string, document: KnowledgeSavedDocument) =>
221
+ [verb, `title: ${document.title}`, `slug: ${document.slug}`, `document_id: ${document.id}`].join(
222
+ '\n'
223
+ )
224
+
225
+ type KnowledgeLookupResultOutput = {
226
+ readonly document: KnowledgeDocument
227
+ score?: number
228
+ context?: ReadonlyArray<{ readonly content: string }>
41
229
  }
42
230
 
43
- const resolveScope = <Context>(resolver: KnowledgeSearchToolScopeResolver<Context>, context: Context) => {
44
- if (typeof resolver === 'function') {
45
- return resolver(context)
231
+ const lookupResultOutput = (result: KnowledgeLookupResult) => {
232
+ const output: KnowledgeLookupResultOutput = { document: result.document }
233
+
234
+ if (result.score !== undefined) output.score = result.score
235
+
236
+ if (result.context !== undefined) {
237
+ output.context = result.context.map(chunk => ({ content: chunk.content }))
46
238
  }
47
239
 
48
- return Effect.succeed(resolver)
240
+ return output
49
241
  }
50
242
 
51
- export const makeKnowledgeSearchTool = <Context>(
52
- searcher: KnowledgeSearcher,
53
- options: MakeKnowledgeSearchToolOptions<Context>
243
+ const savedResult = (input: {
244
+ readonly name: string
245
+ readonly callId: string
246
+ readonly verb: string
247
+ readonly operation: KnowledgeManageOutput['operation']
248
+ readonly document: KnowledgeSavedDocument
249
+ }) =>
250
+ encodeManageOutput({
251
+ operation: input.operation,
252
+ document: { id: input.document.id, slug: input.document.slug, title: input.document.title }
253
+ }).pipe(
254
+ Effect.mapError(outputError(input.name)),
255
+ Effect.map(structuredContent =>
256
+ ToolResult.make({
257
+ toolCallId: input.callId,
258
+ content: formatSaved(input.verb, input.document),
259
+ structuredContent
260
+ })
261
+ )
262
+ )
263
+
264
+ export const makeKnowledgeLookupTool = <Context>(
265
+ handlers: KnowledgeLookupHandlers<Context>,
266
+ options: { readonly name?: string; readonly description?: string } = {}
54
267
  ): ToolRegistration<Context> => {
55
- const name = options.name ?? 'search_knowledge'
268
+ const name = options.name ?? 'knowledge_lookup'
56
269
 
57
270
  return makeTool({
58
271
  name,
59
- description: options.description ?? 'Search the configured knowledge search.',
60
- parameters: KnowledgeSearchToolParams,
272
+ description:
273
+ options.description ??
274
+ 'Look up durable knowledge. Search for semantic discovery; get when document_id or slug is known.',
275
+ parameters: KnowledgeLookupParams,
276
+ output: KnowledgeLookupOutput,
61
277
  access: 'read',
62
- invalidParamsMessage: error => error instanceof Error ? error.message : String(error),
63
- execute: input =>
64
- resolveScope(options.scope, input.context).pipe(
65
- Effect.flatMap(scope =>
66
- searcher.search({
67
- scope,
68
- query: input.params.query,
69
- limit: options.limit,
70
- minScore: options.minScore,
71
- contextChunks: options.contextChunks
278
+ invalidParamsMessage: error =>
279
+ withToolArgumentsErrorHint(
280
+ `Invalid knowledge lookup arguments: ${error instanceof Error ? error.message : String(error)}`,
281
+ error
282
+ ),
283
+ execute: ({ call, context, params }) =>
284
+ Effect.gen(function* () {
285
+ if (params.operation === 'search') {
286
+ const results = yield* handlers.search({
287
+ context,
288
+ query: params.query,
289
+ limit: optionalValue(params.limit),
290
+ minScore: optionalValue(params.minScore),
291
+ contextChunks: optionalValue(params.contextChunks)
72
292
  })
73
- ),
74
- Effect.map(results => packKnowledgeSearchContext(input.params.query, results)),
75
- Effect.map(
76
- context =>
77
- ToolResult.make({
78
- toolCallId: input.call.id,
79
- content: context.text,
80
- structuredContent: context
293
+
294
+ const structuredContent = yield* encodeLookupOutput({
295
+ operation: 'search',
296
+ results: results.map(lookupResultOutput)
297
+ }).pipe(Effect.mapError(outputError(name)))
298
+
299
+ return ToolResult.make({
300
+ toolCallId: call.id,
301
+ content: formatSearchResults(results),
302
+ structuredContent
303
+ })
304
+ }
305
+
306
+ const document = yield* handlers.get({
307
+ context,
308
+ id: optionalValue(params.id),
309
+ target: optionalTargetFromParams(params.target)
310
+ })
311
+
312
+ const structuredContent = yield* encodeLookupOutput({ operation: 'get', document }).pipe(
313
+ Effect.mapError(outputError(name))
314
+ )
315
+
316
+ return ToolResult.make({
317
+ toolCallId: call.id,
318
+ content: formatDocument(document),
319
+ structuredContent
320
+ })
321
+ })
322
+ })
323
+ }
324
+
325
+ export const makeKnowledgeManageTool = <Context>(
326
+ handlers: KnowledgeManageHandlers<Context>,
327
+ options: { readonly name?: string; readonly description?: string } = {}
328
+ ): ToolRegistration<Context> => {
329
+ const name = options.name ?? 'knowledge_manage'
330
+
331
+ return makeTool({
332
+ name,
333
+ description:
334
+ options.description ??
335
+ 'Create, replace, pin, archive, rename, or delete durable knowledge. Use only when explicitly asked.',
336
+ parameters: KnowledgeManageParams,
337
+ output: KnowledgeManageOutput,
338
+ access: 'write',
339
+ invalidParamsMessage: error =>
340
+ withToolArgumentsErrorHint(
341
+ `Invalid knowledge manage arguments: ${error instanceof Error ? error.message : String(error)}`,
342
+ error
343
+ ),
344
+ execute: ({ call, context, params }) =>
345
+ Effect.gen(function* () {
346
+ switch (params.operation) {
347
+ case 'upsert': {
348
+ const document = yield* handlers.upsert({
349
+ context,
350
+ target: targetFromParams(params.target),
351
+ title: params.title,
352
+ purpose: params.purpose,
353
+ origin: params.origin,
354
+ content: params.content,
355
+ availability: params.availability
356
+ })
357
+
358
+ return yield* savedResult({
359
+ name,
360
+ callId: call.id,
361
+ verb: 'Knowledge upserted',
362
+ operation: 'upsert',
363
+ document
364
+ })
365
+ }
366
+
367
+ case 'set_availability': {
368
+ const document = yield* handlers.setAvailability({
369
+ context,
370
+ target: targetFromParams(params.target),
371
+ availability: params.availability
372
+ })
373
+
374
+ return yield* savedResult({
375
+ name,
376
+ callId: call.id,
377
+ verb: 'Knowledge availability updated',
378
+ operation: 'set_availability',
379
+ document
380
+ })
381
+ }
382
+
383
+ case 'rename_slug': {
384
+ const document = yield* handlers.renameSlug({
385
+ context,
386
+ target: targetFromParams(params.target),
387
+ nextSlug: params.nextSlug
388
+ })
389
+
390
+ return yield* savedResult({
391
+ name,
392
+ callId: call.id,
393
+ verb: 'Knowledge slug renamed',
394
+ operation: 'rename_slug',
395
+ document
81
396
  })
82
- ),
83
- Effect.mapError(error => {
84
- if (isToolError(error)) {
85
- return error
86
397
  }
87
398
 
88
- return new ToolError({
89
- tool: input.call.name,
90
- message: error.message,
91
- cause: 'execution'
399
+ case 'delete': {
400
+ const document = yield* handlers.delete({
401
+ context,
402
+ target: targetFromParams(params.target)
403
+ })
404
+
405
+ return yield* savedResult({
406
+ name,
407
+ callId: call.id,
408
+ verb: 'Knowledge deleted',
409
+ operation: 'delete',
410
+ document
411
+ })
412
+ }
413
+ }
414
+
415
+ return yield* Effect.fail(
416
+ new ToolError({
417
+ tool: call.name,
418
+ message: 'Unsupported knowledge operation',
419
+ cause: 'validation'
92
420
  })
93
- })
94
- )
421
+ )
422
+ })
95
423
  })
96
424
  }
package/src/chunking.ts CHANGED
@@ -1,15 +1,22 @@
1
1
  import { Context, Effect, Layer } from 'effect'
2
2
  import { countTokens, decode, encode } from 'gpt-tokenizer/encoding/o200k_base'
3
- import type { KnowledgeChunk, KnowledgeMetadata } from './documents.ts'
3
+ import type {
4
+ KnowledgeChunk,
5
+ KnowledgeDocumentId,
6
+ KnowledgeMetadata,
7
+ KnowledgeScopeId
8
+ } from './documents.ts'
4
9
  import { KnowledgeChunkingError } from './errors.ts'
5
10
 
6
11
  const SENTENCE_PATTERN = /[^.!?]+[.!?]+(?:["')\]]+)?\s*|[^.!?]+$/g
12
+
7
13
  const PARAGRAPH_BREAK_PATTERN = /(\n{2,})/
14
+
8
15
  const WORD_PATTERN = /\S+\s*/g
9
16
 
10
17
  export type ChunkKnowledgeDocumentInput = {
11
- readonly collectionId: string
12
- readonly documentId: string
18
+ readonly scopeId: KnowledgeScopeId
19
+ readonly documentId: KnowledgeDocumentId
13
20
  readonly content: string
14
21
  readonly maxTokens?: number
15
22
  readonly metadata?: KnowledgeMetadata
@@ -52,6 +59,7 @@ const splitSentences = (text: string) =>
52
59
 
53
60
  const splitEncodedTokens = (text: string, maxTokens: number) => {
54
61
  const tokens = encode(text)
62
+
55
63
  return Array.from({ length: Math.ceil(tokens.length / maxTokens) }, (_, index) =>
56
64
  decode(tokens.slice(index * maxTokens, index * maxTokens + maxTokens)).trim()
57
65
  ).filter(chunk => chunk.length > 0)
@@ -68,6 +76,7 @@ const splitOversizedUnit = (text: string, maxTokens: number) => {
68
76
 
69
77
  if (wordTokenCount > maxTokens) {
70
78
  const currentChunk = current.trim()
79
+
71
80
  if (currentChunk.length > 0) {
72
81
  chunks.push(currentChunk)
73
82
  }
@@ -80,6 +89,7 @@ const splitOversizedUnit = (text: string, maxTokens: number) => {
80
89
 
81
90
  if (currentTokenCount > 0 && currentTokenCount + wordTokenCount > maxTokens) {
82
91
  const currentChunk = current.trim()
92
+
83
93
  if (currentChunk.length > 0) {
84
94
  chunks.push(currentChunk)
85
95
  }
@@ -94,6 +104,7 @@ const splitOversizedUnit = (text: string, maxTokens: number) => {
94
104
  }
95
105
 
96
106
  const finalChunk = current.trim()
107
+
97
108
  if (finalChunk.length > 0) {
98
109
  chunks.push(finalChunk)
99
110
  }
@@ -113,6 +124,7 @@ const buildChunkContents = (units: ReadonlyArray<string>, maxTokens: number) =>
113
124
 
114
125
  const pushCurrent = () => {
115
126
  const content = current.trim()
127
+
116
128
  if (content.length > 0) {
117
129
  chunks.push(content)
118
130
  }
@@ -133,6 +145,7 @@ const buildChunkContents = (units: ReadonlyArray<string>, maxTokens: number) =>
133
145
  }
134
146
 
135
147
  pushCurrent()
148
+
136
149
  return chunks
137
150
  }
138
151
 
@@ -145,8 +158,11 @@ export const chunkKnowledgeText = (input: ChunkKnowledgeDocumentInput, maxTokens
145
158
  }
146
159
 
147
160
  const sanitized = sanitizeText(input.content)
161
+
148
162
  if (sanitized.length === 0) {
149
- return yield* Effect.fail(new KnowledgeChunkingError({ message: 'Cannot chunk empty content' }))
163
+ return yield* Effect.fail(
164
+ new KnowledgeChunkingError({ message: 'Cannot chunk empty content' })
165
+ )
150
166
  }
151
167
 
152
168
  const units = splitUnitsBySize(splitSentences(sanitized), maxTokens)
@@ -158,7 +174,7 @@ export const chunkKnowledgeText = (input: ChunkKnowledgeDocumentInput, maxTokens
158
174
 
159
175
  return contents.map((content, position) => ({
160
176
  id: `${input.documentId}:chunk:${position}`,
161
- collectionId: input.collectionId,
177
+ scopeId: input.scopeId,
162
178
  documentId: input.documentId,
163
179
  content,
164
180
  position,
@@ -167,9 +183,12 @@ export const chunkKnowledgeText = (input: ChunkKnowledgeDocumentInput, maxTokens
167
183
  })) satisfies ReadonlyArray<KnowledgeChunk>
168
184
  })
169
185
 
170
- export const makeDefaultKnowledgeChunker = (input: { readonly maxTokens: number }): KnowledgeChunkerApi => ({
186
+ export const makeDefaultKnowledgeChunker = (input: {
187
+ readonly maxTokens: number
188
+ }): KnowledgeChunkerApi => ({
171
189
  chunk: document => chunkKnowledgeText(document, document.maxTokens ?? input.maxTokens)
172
190
  })
173
191
 
174
- export const DefaultKnowledgeChunkerLive = (input: { readonly maxTokens: number } = { maxTokens: 512 }) =>
175
- Layer.succeed(KnowledgeChunker, makeDefaultKnowledgeChunker(input))
192
+ export const DefaultKnowledgeChunkerLive = (
193
+ input: { readonly maxTokens: number } = { maxTokens: 512 }
194
+ ) => Layer.succeed(KnowledgeChunker, makeDefaultKnowledgeChunker(input))
package/src/context.ts CHANGED
@@ -1,32 +1,37 @@
1
1
  import { Array as Arr } from 'effect'
2
- import type { KnowledgeRecord } from './records.ts'
3
- import type { KnowledgeRepresentation } from './representations.ts'
4
-
5
- export type KnowledgeContextItem = {
6
- readonly record: KnowledgeRecord
7
- readonly representation?: KnowledgeRepresentation
8
- }
2
+ import type { KnowledgeDocument } from './documents.ts'
9
3
 
10
4
  export type BuildKnowledgeContextInput = {
11
- readonly items: ReadonlyArray<KnowledgeContextItem>
5
+ readonly documents: ReadonlyArray<KnowledgeDocument>
12
6
  readonly maxCharacters: number
13
7
  }
14
8
 
15
9
  const truncationMarker = '\n\n[truncated: pinned knowledge exceeded context budget]'
16
10
 
17
- const recordHeader = (record: KnowledgeRecord) =>
18
- `## ${record.title}\nrole: ${record.role}\nstatus: ${record.status}\ncontext: ${record.contextPolicy}`
11
+ const documentHeader = (document: KnowledgeDocument) =>
12
+ [
13
+ `## ${document.title}`,
14
+ `slug: ${document.slug}`,
15
+ `purpose: ${document.purpose}`,
16
+ `origin: ${document.origin}`,
17
+ `status: ${document.status}`,
18
+ `availability: ${document.availability}`,
19
+ document.reviewedAt === undefined ? undefined : `reviewed_at: ${document.reviewedAt.toString()}`
20
+ ]
21
+ .filter(line => line !== undefined && line.trim().length > 0)
22
+ .join('\n')
19
23
 
20
- const itemBody = (item: KnowledgeContextItem) => {
21
- const representationText = item.representation?.summary ?? item.representation?.contentText
22
- return [recordHeader(item.record), item.record.summary, representationText]
24
+ const documentBody = (document: KnowledgeDocument) =>
25
+ [documentHeader(document), document.summary, document.content]
23
26
  .filter(section => section !== undefined && section.trim().length > 0)
24
27
  .join('\n')
25
- }
26
28
 
27
29
  export const buildKnowledgeContext = (input: BuildKnowledgeContextInput) => {
28
- const sections = Arr.map(input.items, itemBody).filter(section => section.trim().length > 0)
29
- const header = '# Pinned knowledge\nUse this durable user knowledge as high-priority context. Cite knowledge records when relevant.'
30
+ const sections = Arr.map(input.documents, documentBody).filter(
31
+ section => section.trim().length > 0
32
+ )
33
+
34
+ const header = '# Pinned knowledge\nUse this durable knowledge as high-priority context.'
30
35
  const body = sections.join('\n\n')
31
36
  const context = body.length === 0 ? '' : `${header}\n\n${body}`
32
37