tinker-agent 2.0.0 → 2.2.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 (51) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +27 -2
  3. package/package.json +2 -1
  4. package/src/agent/context-meter.ts +2 -4
  5. package/src/agent/runtime-session.ts +9 -2
  6. package/src/agent/session-ledger.ts +12 -5
  7. package/src/agent/tool-result-content.ts +76 -0
  8. package/src/agent/types.ts +14 -2
  9. package/src/cli/config.ts +4 -0
  10. package/src/cli/model-profiles.ts +41 -2
  11. package/src/cli/public-config-contract.ts +30 -7
  12. package/src/cli/runner-dependencies.ts +5 -0
  13. package/src/cli/tui-memory.ts +1 -0
  14. package/src/cli/tui-runner.tsx +4 -0
  15. package/src/context/compiled-context-hash.ts +2 -1
  16. package/src/context/compiled-context-validator.ts +13 -4
  17. package/src/context/context-protocol-validator.ts +33 -2
  18. package/src/context/context-revision-compiler.ts +2 -1
  19. package/src/context/context-revision.ts +8 -2
  20. package/src/context/context-swap-renderer.ts +46 -12
  21. package/src/context/prefix-retirement-planner.ts +13 -9
  22. package/src/context/protocol-frame.ts +74 -7
  23. package/src/context/swap-planner.ts +19 -14
  24. package/src/events/observation-text-log.ts +1 -1
  25. package/src/events/stdout-event-printer.ts +6 -0
  26. package/src/image/image-asset-store.ts +32 -3
  27. package/src/memory/contracts.ts +63 -3
  28. package/src/memory/memory-coordinator.ts +319 -49
  29. package/src/memory/memory-extractor.ts +48 -48
  30. package/src/memory/memory-get-tool.ts +86 -0
  31. package/src/memory/memory-search-tool.ts +122 -33
  32. package/src/memory/memory-store.ts +227 -20
  33. package/src/model/fake-model-client.ts +129 -76
  34. package/src/model/model-client.ts +62 -11
  35. package/src/model/openai-chat-mapping.ts +2 -1
  36. package/src/model/openai-chat-model-client.ts +22 -10
  37. package/src/model/openai-model-utils.ts +61 -30
  38. package/src/model/openai-responses-mapping.ts +25 -1
  39. package/src/model/openai-responses-model-client.ts +27 -11
  40. package/src/model/token-estimator.ts +10 -0
  41. package/src/observation/observation-builder.ts +100 -25
  42. package/src/session/session-history-reader.ts +128 -5
  43. package/src/session/session-schema.ts +59 -9
  44. package/src/session/session-store.ts +343 -196
  45. package/src/tools/registry.ts +18 -0
  46. package/src/tools/types.ts +46 -0
  47. package/src/tools/view-image.ts +89 -0
  48. package/src/tools/wait.ts +85 -0
  49. package/src/tui/components/memory-browser.tsx +3 -0
  50. package/src/tui/components/prompt-input.tsx +48 -25
  51. package/src/tui/event-store.ts +61 -2
@@ -5,14 +5,16 @@ import path from "node:path";
5
5
  import { Database } from "bun:sqlite";
6
6
  import { createUuidV7, isCanonicalUuidV7 } from "../ids/uuid-v7";
7
7
  import {
8
- MAX_MEMORIES_PER_TURN,
8
+ MAX_MEMORY_SUMMARY_BYTES,
9
9
  MAX_MEMORY_TEXT_BYTES,
10
10
  MEMORY_SCHEMA_VERSION,
11
11
  MEMORY_SEARCH_LIMIT,
12
12
  MemoryError,
13
13
  type MemoryEmbeddingIdentity,
14
+ type MemoryFtsMatch,
14
15
  type MemoryPaths,
15
16
  type MemorySearchMatch,
17
+ type StoredMemoryRecord,
16
18
  type StoredMemorySummary,
17
19
  type MemoryWriteBatch,
18
20
  type MemoryWriteResult,
@@ -34,6 +36,7 @@ const CREATE_MEMORY_META_SQL = `CREATE TABLE memory_meta (
34
36
  const CREATE_MEMORIES_SQL = `CREATE TABLE memories (
35
37
  memory_id TEXT PRIMARY KEY,
36
38
  text TEXT NOT NULL,
39
+ summary TEXT NOT NULL DEFAULT '',
37
40
  text_sha256 TEXT NOT NULL UNIQUE,
38
41
  embedding BLOB NOT NULL,
39
42
  source_workspace TEXT NOT NULL,
@@ -45,6 +48,16 @@ const CREATE_MEMORIES_SQL = `CREATE TABLE memories (
45
48
  const CREATE_MEMORIES_INDEX_SQL = `CREATE INDEX memories_created_at
46
49
  ON memories(created_at DESC)`;
47
50
 
51
+ const MEMORIES_FTS_TABLE = "memories_fts";
52
+
53
+ const CREATE_MEMORIES_FTS_SQL = `CREATE VIRTUAL TABLE memories_fts USING fts5(
54
+ text,
55
+ summary,
56
+ content='memories',
57
+ content_rowid='rowid',
58
+ tokenize='trigram'
59
+ )`;
60
+
48
61
  const EXPECTED_SCHEMA = new Map([
49
62
  ["index:memories_created_at", CREATE_MEMORIES_INDEX_SQL],
50
63
  ["table:memories", CREATE_MEMORIES_SQL],
@@ -72,6 +85,7 @@ export function resolveMemoryPaths(homeRoot = os.homedir()): MemoryPaths {
72
85
  export class MemoryStore {
73
86
  readonly paths: MemoryPaths;
74
87
  readonly dimensions: number;
88
+ readonly ftsAvailable: boolean;
75
89
  private closed = false;
76
90
 
77
91
  private constructor(
@@ -79,10 +93,12 @@ export class MemoryStore {
79
93
  input: Required<Pick<OpenMemoryStoreInput, "clock" | "createMemoryId">> & {
80
94
  readonly paths: MemoryPaths;
81
95
  readonly embedding: MemoryEmbeddingIdentity;
96
+ readonly ftsAvailable: boolean;
82
97
  },
83
98
  ) {
84
99
  this.paths = input.paths;
85
100
  this.dimensions = input.embedding.dimensions;
101
+ this.ftsAvailable = input.ftsAvailable;
86
102
  this.clock = input.clock;
87
103
  this.createMemoryId = input.createMemoryId;
88
104
  }
@@ -120,7 +136,13 @@ export class MemoryStore {
120
136
  safeIntegers: true,
121
137
  });
122
138
  configureDatabase(database, busyTimeoutMs);
123
- initializeOrVerifySchema(database, input.embedding);
139
+ initializeOrVerifySchema(database, input.embedding, paths.database);
140
+ let ftsAvailable = true;
141
+ try {
142
+ ensureFtsIndex(database);
143
+ } catch {
144
+ ftsAvailable = false;
145
+ }
124
146
  await secureCreatedAuxiliaryFile(`${paths.database}-wal`, walExisted);
125
147
  await secureCreatedAuxiliaryFile(`${paths.database}-shm`, shmExisted);
126
148
  await validatePrivateFile(paths.database);
@@ -130,6 +152,7 @@ export class MemoryStore {
130
152
  embedding: Object.freeze({ ...input.embedding }),
131
153
  clock: input.clock ?? (() => new Date().toISOString()),
132
154
  createMemoryId: input.createMemoryId ?? createUuidV7,
155
+ ftsAvailable,
133
156
  });
134
157
  } catch (error) {
135
158
  database?.close();
@@ -163,11 +186,16 @@ export class MemoryStore {
163
186
  runImmediateTransaction(this.database, () => {
164
187
  const insert = this.database.query(
165
188
  `INSERT INTO memories (
166
- memory_id, text, text_sha256, embedding, source_workspace,
189
+ memory_id, text, summary, text_sha256, embedding, source_workspace,
167
190
  source_session_id, source_turn_id, created_at
168
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
191
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
169
192
  ON CONFLICT(text_sha256) DO NOTHING`,
170
193
  );
194
+ const ftsInsert = this.ftsAvailable
195
+ ? this.database.query(
196
+ `INSERT INTO ${MEMORIES_FTS_TABLE}(rowid, text, summary) VALUES (?, ?, ?)`,
197
+ )
198
+ : undefined;
171
199
  for (const candidate of input.candidates) {
172
200
  const memoryId = this.createMemoryId();
173
201
  if (!isCanonicalUuidV7(memoryId)) {
@@ -179,6 +207,7 @@ export class MemoryStore {
179
207
  const result = insert.run(
180
208
  memoryId,
181
209
  candidate.text,
210
+ candidate.summary,
182
211
  sha256(candidate.text),
183
212
  encodeEmbedding(candidate.embedding),
184
213
  input.workspaceRoot,
@@ -194,6 +223,7 @@ export class MemoryStore {
194
223
  );
195
224
  }
196
225
  if (changes === 1) {
226
+ ftsInsert?.run(result.lastInsertRowid, candidate.text, candidate.summary);
197
227
  inserted.push(
198
228
  Object.freeze({
199
229
  memoryId,
@@ -236,7 +266,8 @@ export class MemoryStore {
236
266
  const matches: MemorySearchMatch[] = [];
237
267
  const rows = this.database
238
268
  .query(
239
- `SELECT memory_id, text, embedding, source_workspace, created_at
269
+ `SELECT memory_id, text, summary, embedding, source_workspace,
270
+ source_session_id, created_at
240
271
  FROM memories`,
241
272
  )
242
273
  .iterate();
@@ -247,8 +278,10 @@ export class MemoryStore {
247
278
  Object.freeze({
248
279
  memoryId: sqlString(row.memory_id, "memory_id"),
249
280
  text: sqlString(row.text, "memory text"),
281
+ summary: sqlSummary(row.summary, "memory summary"),
250
282
  score: cosineFromNormalized(queryEmbedding, embedding),
251
283
  sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
284
+ sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
252
285
  createdAt: requireUtcTimestamp(
253
286
  sqlString(row.created_at, "memory created_at"),
254
287
  "memory created_at",
@@ -265,12 +298,74 @@ export class MemoryStore {
265
298
  return Object.freeze(matches.slice(0, limit));
266
299
  }
267
300
 
301
+ searchFts(
302
+ keywords: readonly string[],
303
+ limit = MEMORY_SEARCH_LIMIT,
304
+ ): readonly MemoryFtsMatch[] {
305
+ this.requireOpen();
306
+ if (!this.ftsAvailable) {
307
+ throw new MemoryError(
308
+ "memory_fts_unavailable",
309
+ "Global memory keyword index is unavailable.",
310
+ );
311
+ }
312
+ if (!Number.isSafeInteger(limit) || limit < 1) {
313
+ throw new MemoryError(
314
+ "memory_search_limit_invalid",
315
+ "Memory search limit must be a positive safe integer.",
316
+ );
317
+ }
318
+ const matchExpression = buildFtsMatchExpression(keywords);
319
+ if (matchExpression === null) {
320
+ return Object.freeze([]);
321
+ }
322
+ const matches: MemoryFtsMatch[] = [];
323
+ const rows = this.database
324
+ .query(
325
+ `SELECT m.memory_id, m.text, m.summary, m.source_workspace,
326
+ m.source_session_id, m.created_at,
327
+ bm25(${MEMORIES_FTS_TABLE}, 10.0, 1.0) AS bm25
328
+ FROM ${MEMORIES_FTS_TABLE}
329
+ JOIN memories AS m ON m.rowid = ${MEMORIES_FTS_TABLE}.rowid
330
+ WHERE ${MEMORIES_FTS_TABLE} MATCH ?
331
+ ORDER BY bm25 ASC, m.created_at DESC, m.memory_id ASC
332
+ LIMIT ?`,
333
+ )
334
+ .iterate(matchExpression, limit);
335
+ for (const rowValue of rows) {
336
+ const row = sqlRecord(rowValue, "memory fts row");
337
+ const bm25 = Number(row.bm25);
338
+ if (!Number.isFinite(bm25)) {
339
+ throw new MemoryError(
340
+ "memory_store_read_failed",
341
+ "Memory FTS bm25 score must be finite.",
342
+ );
343
+ }
344
+ matches.push(
345
+ Object.freeze({
346
+ memoryId: sqlString(row.memory_id, "memory_id"),
347
+ text: sqlString(row.text, "memory text"),
348
+ summary: sqlSummary(row.summary, "memory summary"),
349
+ bm25,
350
+ sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
351
+ sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
352
+ createdAt: requireUtcTimestamp(
353
+ sqlString(row.created_at, "memory created_at"),
354
+ "memory created_at",
355
+ ),
356
+ }),
357
+ );
358
+ }
359
+ return Object.freeze(matches);
360
+ }
361
+
268
362
  listStoredMemories(): readonly StoredMemorySummary[] {
269
363
  this.requireOpen();
270
364
  const memories: StoredMemorySummary[] = [];
271
365
  const rows = this.database
272
366
  .query(
273
- `SELECT memory_id, text, source_workspace, created_at
367
+ `SELECT memory_id, text, summary, source_workspace, source_session_id,
368
+ created_at
274
369
  FROM memories
275
370
  ORDER BY created_at DESC, memory_id DESC`,
276
371
  )
@@ -281,7 +376,9 @@ export class MemoryStore {
281
376
  Object.freeze({
282
377
  memoryId: sqlString(row.memory_id, "memory_id"),
283
378
  text: sqlString(row.text, "memory text"),
379
+ summary: sqlSummary(row.summary, "memory summary"),
284
380
  sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
381
+ sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
285
382
  createdAt: requireUtcTimestamp(
286
383
  sqlString(row.created_at, "memory created_at"),
287
384
  "memory created_at",
@@ -292,6 +389,34 @@ export class MemoryStore {
292
389
  return Object.freeze(memories);
293
390
  }
294
391
 
392
+ getById(memoryId: string): StoredMemoryRecord | undefined {
393
+ this.requireOpen();
394
+ const rowValue = this.database
395
+ .query(
396
+ `SELECT memory_id, text, summary, source_workspace, source_session_id,
397
+ source_turn_id, created_at
398
+ FROM memories
399
+ WHERE memory_id = ?`,
400
+ )
401
+ .get(memoryId);
402
+ if (rowValue === null) {
403
+ return undefined;
404
+ }
405
+ const row = sqlRecord(rowValue, "memory row");
406
+ return Object.freeze({
407
+ memoryId: sqlString(row.memory_id, "memory_id"),
408
+ text: sqlString(row.text, "memory text"),
409
+ summary: sqlSummary(row.summary, "memory summary"),
410
+ sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
411
+ sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
412
+ sourceTurnId: sqlString(row.source_turn_id, "memory source_turn_id"),
413
+ createdAt: requireUtcTimestamp(
414
+ sqlString(row.created_at, "memory created_at"),
415
+ "memory created_at",
416
+ ),
417
+ });
418
+ }
419
+
295
420
  count(): number {
296
421
  this.requireOpen();
297
422
  const row = this.database.query("SELECT COUNT(*) AS count FROM memories").get();
@@ -350,6 +475,7 @@ function configureDatabase(database: Database, busyTimeoutMs: number): void {
350
475
  function initializeOrVerifySchema(
351
476
  database: Database,
352
477
  embedding: MemoryEmbeddingIdentity,
478
+ databasePath: string,
353
479
  ): void {
354
480
  let started = false;
355
481
  try {
@@ -367,7 +493,7 @@ function initializeOrVerifySchema(
367
493
  insert.run(key, value);
368
494
  }
369
495
  }
370
- verifySchema(database, embedding);
496
+ verifySchema(database, embedding, databasePath);
371
497
  database.exec("COMMIT");
372
498
  started = false;
373
499
  } catch (error) {
@@ -391,11 +517,15 @@ function initializeOrVerifySchema(
391
517
  }
392
518
  }
393
519
 
394
- function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): void {
520
+ function verifySchema(
521
+ database: Database,
522
+ embedding: MemoryEmbeddingIdentity,
523
+ databasePath: string,
524
+ ): void {
395
525
  const objects = applicationSchemaObjects(database);
396
526
  if (objects.length !== EXPECTED_SCHEMA.size) {
397
- throw new MemoryError(
398
- "memory_schema_invalid",
527
+ throw unsupportedSchemaError(
528
+ databasePath,
399
529
  "Global memory schema has unexpected objects.",
400
530
  );
401
531
  }
@@ -403,8 +533,8 @@ function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): v
403
533
  const key = `${row.type}:${row.name}`;
404
534
  const expected = EXPECTED_SCHEMA.get(key);
405
535
  if (expected === undefined || normalizeSql(row.sql) !== normalizeSql(expected)) {
406
- throw new MemoryError(
407
- "memory_schema_invalid",
536
+ throw unsupportedSchemaError(
537
+ databasePath,
408
538
  `Global memory schema object ${key} is incompatible.`,
409
539
  );
410
540
  }
@@ -434,11 +564,16 @@ function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): v
434
564
  const identityMismatch = identityKeys.some(
435
565
  (key) => actual.has(key) && actual.get(key) !== expected.get(key),
436
566
  );
437
- throw new MemoryError(
438
- identityMismatch ? "memory_embedding_identity_mismatch" : "memory_schema_invalid",
439
- identityMismatch
440
- ? "Configured embedding profile does not match the existing global memory database."
441
- : "Global memory metadata is invalid.",
567
+ if (identityMismatch) {
568
+ throw new MemoryError(
569
+ "memory_embedding_identity_mismatch",
570
+ "Configured embedding profile does not match the existing global memory database.",
571
+ );
572
+ }
573
+ throw unsupportedSchemaError(
574
+ databasePath,
575
+ "Global memory metadata does not match schema version " +
576
+ `${MEMORY_SCHEMA_VERSION}.`,
442
577
  );
443
578
  }
444
579
 
@@ -446,6 +581,15 @@ function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): v
446
581
  expectedEmbeddingBlobBytes(storedDimensions);
447
582
  }
448
583
 
584
+ function unsupportedSchemaError(databasePath: string, detail: string): MemoryError {
585
+ return new MemoryError(
586
+ "memory_schema_unsupported",
587
+ `${detail} Delete ${databasePath} (including ${databasePath}-wal and ` +
588
+ `${databasePath}-shm) and restart to finish the memory schema v` +
589
+ `${MEMORY_SCHEMA_VERSION} upgrade.`,
590
+ );
591
+ }
592
+
449
593
  function applicationSchemaObjects(
450
594
  database: Database,
451
595
  ): Array<{ readonly type: string; readonly name: string; readonly sql: string }> {
@@ -454,6 +598,7 @@ function applicationSchemaObjects(
454
598
  `SELECT type, name, sql
455
599
  FROM sqlite_schema
456
600
  WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'
601
+ AND name <> '${MEMORIES_FTS_TABLE}' AND name NOT LIKE '${MEMORIES_FTS_TABLE}\\_%' ESCAPE '\\'
457
602
  ORDER BY type, name`,
458
603
  )
459
604
  .all()
@@ -479,6 +624,60 @@ function metadataEntries(
479
624
  ]);
480
625
  }
481
626
 
627
+ function ensureFtsIndex(database: Database): void {
628
+ runImmediateTransaction(database, () => {
629
+ const rowValue = database
630
+ .query(
631
+ `SELECT sql FROM sqlite_schema
632
+ WHERE type = 'table' AND name = '${MEMORIES_FTS_TABLE}'`,
633
+ )
634
+ .get();
635
+ if (rowValue === null) {
636
+ database.exec(CREATE_MEMORIES_FTS_SQL);
637
+ backfillFtsIndex(database);
638
+ return;
639
+ }
640
+ const row = sqlRecord(rowValue, "memories_fts schema");
641
+ const sql = sqlString(row.sql, "memories_fts SQL");
642
+ if (normalizeSql(sql) !== normalizeSql(CREATE_MEMORIES_FTS_SQL)) {
643
+ database.exec(`DROP TABLE ${MEMORIES_FTS_TABLE}`);
644
+ database.exec(CREATE_MEMORIES_FTS_SQL);
645
+ backfillFtsIndex(database);
646
+ return;
647
+ }
648
+ const memoryCount = countRows(database, "memories");
649
+ const ftsCount = countRows(database, `${MEMORIES_FTS_TABLE}_docsize`);
650
+ if (memoryCount !== ftsCount) {
651
+ database.exec(
652
+ `INSERT INTO ${MEMORIES_FTS_TABLE}(${MEMORIES_FTS_TABLE}) VALUES('rebuild')`,
653
+ );
654
+ }
655
+ });
656
+ }
657
+
658
+ function backfillFtsIndex(database: Database): void {
659
+ database.exec(
660
+ `INSERT INTO ${MEMORIES_FTS_TABLE}(rowid, text, summary)
661
+ SELECT rowid, text, summary FROM memories`,
662
+ );
663
+ }
664
+
665
+ function countRows(database: Database, table: string): number {
666
+ const row = sqlRecord(
667
+ database.query(`SELECT COUNT(*) AS count FROM ${table}`).get(),
668
+ `${table} count`,
669
+ );
670
+ return Number(row.count);
671
+ }
672
+
673
+ export function buildFtsMatchExpression(keywords: readonly string[]): string | null {
674
+ const phrases = keywords
675
+ .map((keyword) => keyword.trim())
676
+ .filter((keyword) => [...keyword].length >= 3)
677
+ .map((keyword) => `"${keyword.replaceAll('"', '""')}"`);
678
+ return phrases.length === 0 ? null : phrases.join(" OR ");
679
+ }
680
+
482
681
  function runImmediateTransaction(database: Database, operation: () => void): void {
483
682
  let started = false;
484
683
  try {
@@ -519,10 +718,10 @@ function validateWriteBatch(input: MemoryWriteBatch, dimensions: number): void {
519
718
  "Memory source session and turn IDs must not be empty.",
520
719
  );
521
720
  }
522
- if (input.candidates.length > MAX_MEMORIES_PER_TURN) {
721
+ if (input.candidates.length > 1) {
523
722
  throw new MemoryError(
524
723
  "memory_write_invalid",
525
- `A memory batch may contain at most ${MAX_MEMORIES_PER_TURN} candidates.`,
724
+ "A memory batch may contain at most 1 candidate.",
526
725
  );
527
726
  }
528
727
  for (const candidate of input.candidates) {
@@ -530,11 +729,12 @@ function validateWriteBatch(input: MemoryWriteBatch, dimensions: number): void {
530
729
  candidate.text.trim() !== candidate.text ||
531
730
  Buffer.byteLength(candidate.text, "utf8") < 1 ||
532
731
  Buffer.byteLength(candidate.text, "utf8") > MAX_MEMORY_TEXT_BYTES ||
732
+ Buffer.byteLength(candidate.summary, "utf8") > MAX_MEMORY_SUMMARY_BYTES ||
533
733
  candidate.embedding.length !== dimensions
534
734
  ) {
535
735
  throw new MemoryError(
536
736
  "memory_write_invalid",
537
- "Memory candidate text or embedding is invalid.",
737
+ "Memory candidate text, summary, or embedding is invalid.",
538
738
  );
539
739
  }
540
740
  }
@@ -664,6 +864,13 @@ function sqlString(value: unknown, name: string): string {
664
864
  return value;
665
865
  }
666
866
 
867
+ function sqlSummary(value: unknown, name: string): string {
868
+ if (typeof value !== "string") {
869
+ throw new MemoryError("memory_store_read_failed", `${name} must be a string.`);
870
+ }
871
+ return value;
872
+ }
873
+
667
874
  function requireUtcTimestamp(value: string, name: string): string {
668
875
  if (!value.endsWith("Z") || Number.isNaN(Date.parse(value))) {
669
876
  throw new MemoryError(