opencodekit 0.23.1 → 0.23.2

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.
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { ObservationInput, ObservationRow } from "./db/types.js";
17
- import { getMemoryDB } from "./db.js";
17
+ import { getMemoryDB } from "./db/schema.js";
18
18
  import { hasWord } from "./helpers.js";
19
19
  import { appendOperationLog } from "./operation-log.js";
20
20
 
@@ -25,17 +25,17 @@ import { appendOperationLog } from "./operation-log.js";
25
25
  export type ValidationVerdict = "pass" | "warn" | "reject";
26
26
 
27
27
  export interface ValidationResult {
28
- verdict: ValidationVerdict;
29
- issues: ValidationIssue[];
30
- /** If a near-duplicate was found, its ID */
31
- duplicateOf?: number;
28
+ verdict: ValidationVerdict;
29
+ issues: ValidationIssue[];
30
+ /** If a near-duplicate was found, its ID */
31
+ duplicateOf?: number;
32
32
  }
33
33
 
34
34
  export interface ValidationIssue {
35
- type: "duplicate" | "near-duplicate" | "contradiction" | "low-quality";
36
- severity: "high" | "medium" | "low";
37
- message: string;
38
- relatedId?: number;
35
+ type: "duplicate" | "near-duplicate" | "contradiction" | "low-quality";
36
+ severity: "high" | "medium" | "low";
37
+ message: string;
38
+ relatedId?: number;
39
39
  }
40
40
 
41
41
  // ============================================================================
@@ -47,87 +47,82 @@ export interface ValidationIssue {
47
47
  * Returns verdict (pass/warn/reject) with issues.
48
48
  */
49
49
  export function validateObservation(input: ObservationInput): ValidationResult {
50
- const issues: ValidationIssue[] = [];
51
-
52
- // Check 1: Exact title duplicate
53
- const exactDup = findExactDuplicate(input);
54
- if (exactDup) {
55
- // If it's explicitly superseding, that's fine
56
- if (input.supersedes === exactDup.id) {
57
- // Intentional supersede — pass
58
- } else {
59
- issues.push({
60
- type: "duplicate",
61
- severity: "high",
62
- message: `Exact duplicate of #${exactDup.id}: "${exactDup.title}"`,
63
- relatedId: exactDup.id,
64
- });
65
-
66
- appendOperationLog({
67
- operation: "observation-duplicate-warning",
68
- targets: [`#${exactDup.id}`],
69
- summary: `Duplicate warning: "${input.title}" (matches #${exactDup.id})`,
70
- });
71
-
72
- return {
73
- verdict: "warn",
74
- issues,
75
- duplicateOf: exactDup.id,
76
- };
77
- }
78
- }
79
-
80
- // Check 2: Near-duplicate via FTS5
81
- const nearDup = findNearDuplicate(input);
82
- if (nearDup) {
83
- issues.push({
84
- type: "near-duplicate",
85
- severity: "medium",
86
- message: `Similar to #${nearDup.id}: "${nearDup.title}" (consider superseding)`,
87
- relatedId: nearDup.id,
88
- });
89
- }
90
-
91
- // Check 3: Contradiction with existing decisions
92
- if (input.type === "decision") {
93
- const contradiction = findContradiction(input);
94
- if (contradiction) {
95
- issues.push({
96
- type: "contradiction",
97
- severity: "medium",
98
- message: `May contradict #${contradiction.id}: "${contradiction.title}"`,
99
- relatedId: contradiction.id,
100
- });
101
- }
102
- }
103
-
104
- // Check 4: Low quality (no narrative, no concepts)
105
- if (!input.narrative && !input.concepts) {
106
- issues.push({
107
- type: "low-quality",
108
- severity: "low",
109
- message:
110
- "Observation has no narrative and no concepts — low knowledge value",
111
- });
112
- }
113
-
114
- // Determine verdict
115
- const hasHigh = issues.some((i) => i.severity === "high");
116
- const verdict: ValidationVerdict = hasHigh
117
- ? "reject"
118
- : issues.length > 0
119
- ? "warn"
120
- : "pass";
121
-
122
- if (verdict === "pass" || verdict === "warn") {
123
- appendOperationLog({
124
- operation: "observation-validated",
125
- targets: [input.title],
126
- summary: `Validated [${input.type}] "${input.title}" — ${verdict}${issues.length > 0 ? ` (${issues.length} issues)` : ""}`,
127
- });
128
- }
129
-
130
- return { verdict, issues };
50
+ const issues: ValidationIssue[] = [];
51
+
52
+ // Check 1: Exact title duplicate
53
+ const exactDup = findExactDuplicate(input);
54
+ if (exactDup) {
55
+ // If it's explicitly superseding, that's fine
56
+ if (input.supersedes === exactDup.id) {
57
+ // Intentional supersede — pass
58
+ } else {
59
+ issues.push({
60
+ type: "duplicate",
61
+ severity: "high",
62
+ message: `Exact duplicate of #${exactDup.id}: "${exactDup.title}"`,
63
+ relatedId: exactDup.id,
64
+ });
65
+
66
+ appendOperationLog({
67
+ operation: "observation-duplicate-warning",
68
+ targets: [`#${exactDup.id}`],
69
+ summary: `Duplicate warning: "${input.title}" (matches #${exactDup.id})`,
70
+ });
71
+
72
+ return {
73
+ verdict: "warn",
74
+ issues,
75
+ duplicateOf: exactDup.id,
76
+ };
77
+ }
78
+ }
79
+
80
+ // Check 2: Near-duplicate via FTS5
81
+ const nearDup = findNearDuplicate(input);
82
+ if (nearDup) {
83
+ issues.push({
84
+ type: "near-duplicate",
85
+ severity: "medium",
86
+ message: `Similar to #${nearDup.id}: "${nearDup.title}" (consider superseding)`,
87
+ relatedId: nearDup.id,
88
+ });
89
+ }
90
+
91
+ // Check 3: Contradiction with existing decisions
92
+ if (input.type === "decision") {
93
+ const contradiction = findContradiction(input);
94
+ if (contradiction) {
95
+ issues.push({
96
+ type: "contradiction",
97
+ severity: "medium",
98
+ message: `May contradict #${contradiction.id}: "${contradiction.title}"`,
99
+ relatedId: contradiction.id,
100
+ });
101
+ }
102
+ }
103
+
104
+ // Check 4: Low quality (no narrative, no concepts)
105
+ if (!input.narrative && !input.concepts) {
106
+ issues.push({
107
+ type: "low-quality",
108
+ severity: "low",
109
+ message: "Observation has no narrative and no concepts — low knowledge value",
110
+ });
111
+ }
112
+
113
+ // Determine verdict
114
+ const hasHigh = issues.some((i) => i.severity === "high");
115
+ const verdict: ValidationVerdict = hasHigh ? "reject" : issues.length > 0 ? "warn" : "pass";
116
+
117
+ if (verdict === "pass" || verdict === "warn") {
118
+ appendOperationLog({
119
+ operation: "observation-validated",
120
+ targets: [input.title],
121
+ summary: `Validated [${input.type}] "${input.title}" — ${verdict}${issues.length > 0 ? ` (${issues.length} issues)` : ""}`,
122
+ });
123
+ }
124
+
125
+ return { verdict, issues };
131
126
  }
132
127
 
133
128
  // ============================================================================
@@ -138,106 +133,101 @@ export function validateObservation(input: ObservationInput): ValidationResult {
138
133
  * Check for exact title match (same type, active).
139
134
  */
140
135
  function findExactDuplicate(input: ObservationInput): ObservationRow | null {
141
- const db = getMemoryDB();
142
- return db
143
- .query(
144
- `SELECT * FROM observations
136
+ const db = getMemoryDB();
137
+ return db
138
+ .query(
139
+ `SELECT * FROM observations
145
140
  WHERE type = ? AND LOWER(title) = LOWER(?) AND superseded_by IS NULL
146
141
  LIMIT 1`,
147
- )
148
- .get(input.type, input.title) as ObservationRow | null;
142
+ )
143
+ .get(input.type, input.title) as ObservationRow | null;
149
144
  }
150
145
 
151
146
  /**
152
147
  * Check for near-duplicate via FTS5 title search.
153
148
  */
154
149
  function findNearDuplicate(input: ObservationInput): ObservationRow | null {
155
- const db = getMemoryDB();
156
-
157
- // Build FTS query from title words
158
- // Strip FTS5 special operators and characters to prevent query syntax errors
159
- const FTS5_OPERATORS = /\b(AND|OR|NOT|NEAR)\b/gi;
160
- const words = input.title
161
- .replace(FTS5_OPERATORS, "")
162
- .replace(/['"*^+(){}]/g, "")
163
- .split(/\s+/)
164
- .filter((w) => w.length > 2)
165
- .map((w) => `"${w}"*`)
166
- .join(" AND ");
167
-
168
- if (!words) return null;
169
-
170
- try {
171
- const result = db
172
- .query(
173
- `SELECT o.* FROM observations o
150
+ const db = getMemoryDB();
151
+
152
+ // Build FTS query from title words
153
+ // Strip FTS5 special operators and characters to prevent query syntax errors
154
+ const FTS5_OPERATORS = /\b(AND|OR|NOT|NEAR)\b/gi;
155
+ const words = input.title
156
+ .replace(FTS5_OPERATORS, "")
157
+ .replace(/['"*^+(){}]/g, "")
158
+ .split(/\s+/)
159
+ .filter((w) => w.length > 2)
160
+ .map((w) => `"${w}"*`)
161
+ .join(" AND ");
162
+
163
+ if (!words) return null;
164
+
165
+ try {
166
+ const result = db
167
+ .query(
168
+ `SELECT o.* FROM observations o
174
169
  JOIN observations_fts fts ON fts.rowid = o.id
175
170
  WHERE observations_fts MATCH ?
176
171
  AND o.type = ?
177
172
  AND o.superseded_by IS NULL
178
173
  AND o.id != COALESCE(?, -1)
179
174
  ORDER BY bm25(observations_fts) LIMIT 1`,
180
- )
181
- .get(
182
- words,
183
- input.type,
184
- input.supersedes ?? null,
185
- ) as ObservationRow | null;
186
-
187
- return result;
188
- } catch {
189
- return null;
190
- }
175
+ )
176
+ .get(words, input.type, input.supersedes ?? null) as ObservationRow | null;
177
+
178
+ return result;
179
+ } catch {
180
+ return null;
181
+ }
191
182
  }
192
183
 
193
184
  /**
194
185
  * Check for contradictions with existing decisions.
195
186
  */
196
187
  function findContradiction(input: ObservationInput): ObservationRow | null {
197
- if (!input.concepts || input.concepts.length === 0) return null;
188
+ if (!input.concepts || input.concepts.length === 0) return null;
198
189
 
199
- const db = getMemoryDB();
190
+ const db = getMemoryDB();
200
191
 
201
- // Search for decisions with overlapping concepts
202
- const conceptQuery = input.concepts.map((c) => `"${c}"*`).join(" OR ");
192
+ // Search for decisions with overlapping concepts
193
+ const conceptQuery = input.concepts.map((c) => `"${c}"*`).join(" OR ");
203
194
 
204
- try {
205
- const candidates = db
206
- .query(
207
- `SELECT o.* FROM observations o
195
+ try {
196
+ const candidates = db
197
+ .query(
198
+ `SELECT o.* FROM observations o
208
199
  JOIN observations_fts fts ON fts.rowid = o.id
209
200
  WHERE observations_fts MATCH ?
210
201
  AND o.type = 'decision'
211
202
  AND o.superseded_by IS NULL
212
203
  ORDER BY bm25(observations_fts) LIMIT 5`,
213
- )
214
- .all(conceptQuery) as ObservationRow[];
215
-
216
- // Check for opposing signals
217
- const inputText = `${input.title} ${input.narrative ?? ""}`.toLowerCase();
218
- const contradictionPairs = [
219
- ["use", "don't use"],
220
- ["enable", "disable"],
221
- ["add", "remove"],
222
- ["prefer", "avoid"],
223
- ["always", "never"],
224
- ];
225
-
226
- for (const candidate of candidates) {
227
- const candidateText =
228
- `${candidate.title} ${candidate.narrative ?? ""}`.toLowerCase();
229
- for (const [wordA, wordB] of contradictionPairs) {
230
- if (
231
- (hasWord(inputText, wordA) && hasWord(candidateText, wordB)) ||
232
- (hasWord(inputText, wordB) && hasWord(candidateText, wordA))
233
- ) {
234
- return candidate;
235
- }
236
- }
237
- }
238
- } catch {
239
- // FTS5 query failed
240
- }
241
-
242
- return null;
204
+ )
205
+ .all(conceptQuery) as ObservationRow[];
206
+
207
+ // Check for opposing signals
208
+ const inputText = `${input.title} ${input.narrative ?? ""}`.toLowerCase();
209
+ const contradictionPairs = [
210
+ ["use", "don't use"],
211
+ ["enable", "disable"],
212
+ ["add", "remove"],
213
+ ["prefer", "avoid"],
214
+ ["always", "never"],
215
+ ];
216
+
217
+ for (const candidate of candidates) {
218
+ const candidateText = `${candidate.title} ${candidate.narrative ?? ""}`.toLowerCase();
219
+ for (const [wordA, wordB] of contradictionPairs) {
220
+ if (
221
+ (hasWord(inputText, wordA) && hasWord(candidateText, wordB)) ||
222
+ (hasWord(inputText, wordB) && hasWord(candidateText, wordA))
223
+ ) {
224
+ return candidate;
225
+ }
226
+ }
227
+ }
228
+ } catch {
229
+ // FTS5 query failed
230
+ }
231
+
232
+ return null;
243
233
  }
@@ -1,5 +1,6 @@
1
- import { createProviderToolFactory } from "@ai-sdk/provider-utils"
2
- import { z } from "zod/v4"
1
+ import { createProviderToolFactory } from "@ai-sdk/provider-utils";
2
+ import { z } from "zod/v4";
3
+ import { webSearchInputSchema } from "./web-search-shared.js";
3
4
 
4
5
  // Args validation schema
5
6
  export const webSearchPreviewArgsSchema = z.object({
@@ -38,7 +39,7 @@ export const webSearchPreviewArgsSchema = z.object({
38
39
  timezone: z.string().optional(),
39
40
  })
40
41
  .optional(),
41
- })
42
+ });
42
43
 
43
44
  export const webSearchPreview = createProviderToolFactory<
44
45
  {
@@ -51,7 +52,7 @@ export const webSearchPreview = createProviderToolFactory<
51
52
  * - medium: Balanced context, cost, and latency (default)
52
53
  * - low: Least context, lowest cost, fastest response
53
54
  */
54
- searchContextSize?: "low" | "medium" | "high"
55
+ searchContextSize?: "low" | "medium" | "high";
55
56
 
56
57
  /**
57
58
  * User location information to provide geographically relevant search results.
@@ -60,44 +61,26 @@ export const webSearchPreview = createProviderToolFactory<
60
61
  /**
61
62
  * Type of location (always 'approximate')
62
63
  */
63
- type: "approximate"
64
+ type: "approximate";
64
65
  /**
65
66
  * Two-letter ISO country code (e.g., 'US', 'GB')
66
67
  */
67
- country?: string
68
+ country?: string;
68
69
  /**
69
70
  * City name (free text, e.g., 'Minneapolis')
70
71
  */
71
- city?: string
72
+ city?: string;
72
73
  /**
73
74
  * Region name (free text, e.g., 'Minnesota')
74
75
  */
75
- region?: string
76
+ region?: string;
76
77
  /**
77
78
  * IANA timezone (e.g., 'America/Chicago')
78
79
  */
79
- timezone?: string
80
- }
80
+ timezone?: string;
81
+ };
81
82
  }
82
83
  >({
83
84
  id: "openai.web_search_preview",
84
- inputSchema: z.object({
85
- action: z
86
- .discriminatedUnion("type", [
87
- z.object({
88
- type: z.literal("search"),
89
- query: z.string().nullish(),
90
- }),
91
- z.object({
92
- type: z.literal("open_page"),
93
- url: z.string(),
94
- }),
95
- z.object({
96
- type: z.literal("find"),
97
- url: z.string(),
98
- pattern: z.string(),
99
- }),
100
- ])
101
- .nullish(),
102
- }),
103
- })
85
+ inputSchema: webSearchInputSchema,
86
+ });
@@ -0,0 +1,25 @@
1
+ import { z } from "zod/v4";
2
+
3
+ /**
4
+ * Shared input schema for web search tool actions.
5
+ * Both search and search-preview tools use the same action types.
6
+ */
7
+ export const webSearchInputSchema = z.object({
8
+ action: z
9
+ .discriminatedUnion("type", [
10
+ z.object({
11
+ type: z.literal("search"),
12
+ query: z.string().nullish(),
13
+ }),
14
+ z.object({
15
+ type: z.literal("open_page"),
16
+ url: z.string(),
17
+ }),
18
+ z.object({
19
+ type: z.literal("find"),
20
+ url: z.string(),
21
+ pattern: z.string(),
22
+ }),
23
+ ])
24
+ .nullish(),
25
+ });
@@ -1,5 +1,6 @@
1
- import { createProviderToolFactory } from "@ai-sdk/provider-utils"
2
- import { z } from "zod/v4"
1
+ import { createProviderToolFactory } from "@ai-sdk/provider-utils";
2
+ import { z } from "zod/v4";
3
+ import { webSearchInputSchema } from "./web-search-shared.js";
3
4
 
4
5
  export const webSearchArgsSchema = z.object({
5
6
  filters: z
@@ -19,7 +20,7 @@ export const webSearchArgsSchema = z.object({
19
20
  timezone: z.string().optional(),
20
21
  })
21
22
  .optional(),
22
- })
23
+ });
23
24
 
24
25
  export const webSearchToolFactory = createProviderToolFactory<
25
26
  {
@@ -35,8 +36,8 @@ export const webSearchToolFactory = createProviderToolFactory<
35
36
  * If not provided, all domains are allowed.
36
37
  * Subdomains of the provided domains are allowed as well.
37
38
  */
38
- allowedDomains?: string[]
39
- }
39
+ allowedDomains?: string[];
40
+ };
40
41
 
41
42
  /**
42
43
  * Search context size to use for the web search.
@@ -44,7 +45,7 @@ export const webSearchToolFactory = createProviderToolFactory<
44
45
  * - medium: Balanced context, cost, and latency (default)
45
46
  * - low: Least context, lowest cost, fastest response
46
47
  */
47
- searchContextSize?: "low" | "medium" | "high"
48
+ searchContextSize?: "low" | "medium" | "high";
48
49
 
49
50
  /**
50
51
  * User location information to provide geographically relevant search results.
@@ -53,50 +54,32 @@ export const webSearchToolFactory = createProviderToolFactory<
53
54
  /**
54
55
  * Type of location (always 'approximate')
55
56
  */
56
- type: "approximate"
57
+ type: "approximate";
57
58
  /**
58
59
  * Two-letter ISO country code (e.g., 'US', 'GB')
59
60
  */
60
- country?: string
61
+ country?: string;
61
62
  /**
62
63
  * City name (free text, e.g., 'Minneapolis')
63
64
  */
64
- city?: string
65
+ city?: string;
65
66
  /**
66
67
  * Region name (free text, e.g., 'Minnesota')
67
68
  */
68
- region?: string
69
+ region?: string;
69
70
  /**
70
71
  * IANA timezone (e.g., 'America/Chicago')
71
72
  */
72
- timezone?: string
73
- }
73
+ timezone?: string;
74
+ };
74
75
  }
75
76
  >({
76
77
  id: "openai.web_search",
77
- inputSchema: z.object({
78
- action: z
79
- .discriminatedUnion("type", [
80
- z.object({
81
- type: z.literal("search"),
82
- query: z.string().nullish(),
83
- }),
84
- z.object({
85
- type: z.literal("open_page"),
86
- url: z.string(),
87
- }),
88
- z.object({
89
- type: z.literal("find"),
90
- url: z.string(),
91
- pattern: z.string(),
92
- }),
93
- ])
94
- .nullish(),
95
- }),
96
- })
78
+ inputSchema: webSearchInputSchema,
79
+ });
97
80
 
98
81
  export const webSearch = (
99
82
  args: Parameters<typeof webSearchToolFactory>[0] = {}, // default
100
83
  ) => {
101
- return webSearchToolFactory(args)
102
- }
84
+ return webSearchToolFactory(args);
85
+ };