tinker-agent 1.4.0 → 1.5.1

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 (39) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +54 -0
  3. package/package.json +6 -1
  4. package/src/agent/runtime-session.ts +79 -0
  5. package/src/cli/config.ts +29 -0
  6. package/src/cli/model-profiles.ts +84 -1
  7. package/src/cli/public-config-contract.ts +82 -0
  8. package/src/cli/runner-dependencies.ts +8 -0
  9. package/src/cli/tui-memory.ts +67 -0
  10. package/src/cli/tui-runner.tsx +31 -0
  11. package/src/context/context-policy.ts +2 -2
  12. package/src/events/stdout-event-printer.ts +1 -0
  13. package/src/memory/contracts.ts +148 -0
  14. package/src/memory/embedding-client.ts +105 -0
  15. package/src/memory/memory-coordinator.ts +556 -0
  16. package/src/memory/memory-extractor.ts +231 -0
  17. package/src/memory/memory-log.ts +88 -0
  18. package/src/memory/memory-search-tool.ts +100 -0
  19. package/src/memory/memory-store.ts +687 -0
  20. package/src/memory/vector.ts +153 -0
  21. package/src/model/fake-model-client.ts +1068 -3
  22. package/src/observation/observation-builder.ts +20 -0
  23. package/src/session/session-store.ts +123 -0
  24. package/src/tools/registry.ts +4 -0
  25. package/src/tools/types.ts +16 -0
  26. package/src/tui/app.tsx +211 -93
  27. package/src/tui/clipboard.ts +22 -0
  28. package/src/tui/components/assistant-markdown.tsx +27 -26
  29. package/src/tui/components/background-tasks.tsx +7 -2
  30. package/src/tui/components/file-viewer.tsx +2 -2
  31. package/src/tui/components/footer.tsx +5 -6
  32. package/src/tui/components/memory-browser.tsx +151 -0
  33. package/src/tui/components/resume-session-picker.tsx +3 -1
  34. package/src/tui/components/timeline.tsx +9 -11
  35. package/src/tui/event-store.ts +22 -2
  36. package/src/tui/shiki-highlighter.ts +104 -0
  37. package/src/tui/slash-commands.ts +12 -0
  38. package/src/tui/tui-projection-store.ts +33 -0
  39. package/src/tui/tui-session-controller.ts +14 -8
package/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.5.1] - 2026-07-29
9
+
10
+ ### Changed
11
+
12
+ - Keep settled TUI history outside recurring live renders and reuse prepared
13
+ Markdown highlighting, reducing redraw work during long interactive sessions.
14
+ - Stop repainting the TUI once per second solely to update a running elapsed-time
15
+ counter; completed turns still show their final duration.
16
+
17
+ ### Fixed
18
+
19
+ - Restore the visible history tail after closing `/view`, `/memory`, `/resume`,
20
+ and other temporary TUI panels instead of leaving a mostly blank viewport.
21
+ - Keep the `/resume` picker bound to the session that opened it, preventing stale
22
+ picker state from affecting a newly activated session.
23
+
24
+ ## [1.5.0] - 2026-07-27
25
+
26
+ ### Added
27
+
28
+ - Add optional global Memory for the interactive TUI. When configured, Tinker
29
+ extracts durable facts from completed turns, stores them locally, and exposes
30
+ relevant memories to the agent through `MemorySearch`.
31
+ - Add a read-only `/memory` browser for reviewing the memories stored in the
32
+ current global Memory database.
33
+ - Show live elapsed time in the TUI footer while a turn is running.
34
+
35
+ ### Changed
36
+
37
+ - Compact and retire context toward 30% input utilization instead of 60%, leaving
38
+ more headroom for continued work after automatic or manual maintenance.
39
+
40
+ ### Fixed
41
+
42
+ - Let the Tinker Chrome MCP server exit promptly when its client closes stdin,
43
+ avoiding the shutdown timeout during ordinary CLI exit.
44
+
8
45
  ## [1.4.0] - 2026-07-24
9
46
 
10
47
  ### Added
@@ -63,7 +100,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
63
100
  - First formal npm release under the `tinker-agent` package name with the `tinker`
64
101
  executable.
65
102
 
66
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.4.0...HEAD
103
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.5.1...HEAD
104
+ [1.5.1]: https://github.com/ishowshao/tinker/releases/tag/v1.5.1
105
+ [1.5.0]: https://github.com/ishowshao/tinker/releases/tag/v1.5.0
67
106
  [1.4.0]: https://github.com/ishowshao/tinker/releases/tag/v1.4.0
68
107
  [1.3.0]: https://github.com/ishowshao/tinker/releases/tag/v1.3.0
69
108
  [1.2.1]: https://github.com/ishowshao/tinker/releases/tag/v1.2.1
package/README.md CHANGED
@@ -243,6 +243,59 @@ Image-capable profile example:
243
243
  }
244
244
  }
245
245
  ```
246
+
247
+ The top-level `memory` object is optional. When present, every field below is required. It enables completed-turn extraction and `MemorySearch` only in the TUI; one-shot runs do not load memory.
248
+
249
+ | Field | Required | Type / constraint | Secret | Description |
250
+ | --- | --- | --- | --- | --- |
251
+ | `profile` | Yes | Non-empty string | No | Existing model profile used for completed-turn atomic-memory extraction. |
252
+ | `embedding` | Yes | Object | Yes | Single embedding profile for the global memory database. |
253
+
254
+ `memory.embedding` fields:
255
+
256
+ | Field | Required | Type / constraint | Secret | Description |
257
+ | --- | --- | --- | --- | --- |
258
+ | `name` | Yes | Non-empty string | No | Stable identity for the embedding space. |
259
+ | `kind` | Yes | Literal `"openai-compatible"` | No | Embedding transport kind. |
260
+ | `model` | Yes | Non-empty string | No | Embedding provider model name. |
261
+ | `apiBase` | Yes | Non-empty string | No | OpenAI-compatible API base URL. |
262
+ | `apiKey` | Yes | Non-empty string | Yes | Embedding provider credential. |
263
+ | `dimensions` | Yes | Positive integer | No | Fixed vector dimensions for the global memory database. |
264
+
265
+ Enabling memory sends completed-turn text (not image bytes) to `memory.profile`, and sends extracted candidates plus search queries to the embedding endpoint. Derived memories are stored in `~/.tinker/memory/memory.sqlite`; newly inserted memory text is appended to the private development log `~/.tinker/memory/extracted-memories.log`.
266
+
267
+ Atomic-memory profile example:
268
+
269
+ ```json
270
+ {
271
+ "default": "text",
272
+ "profiles": {
273
+ "text": {
274
+ "model": "example-text-model",
275
+ "apiBase": "https://api.example.com/v1",
276
+ "apiKey": "your-model-api-key",
277
+ "contextWindowTokens": 128000,
278
+ "maxSupportedOutputTokens": 8192,
279
+ "includeReasoningContent": false,
280
+ "stream": true,
281
+ "inputModalities": [
282
+ "text"
283
+ ]
284
+ }
285
+ },
286
+ "memory": {
287
+ "profile": "text",
288
+ "embedding": {
289
+ "name": "example-embedding-space",
290
+ "kind": "openai-compatible",
291
+ "model": "example-embedding-model",
292
+ "apiBase": "https://embeddings.example.com/v1",
293
+ "apiKey": "your-embedding-api-key",
294
+ "dimensions": 1024
295
+ }
296
+ }
297
+ }
298
+ ```
246
299
  <!-- END GENERATED: MODEL PROFILE FIELDS -->
247
300
 
248
301
  You can also select profiles explicitly for the TUI or one-shot command:
@@ -282,6 +335,7 @@ complete fixed policy and persistence contract.
282
335
  | `/status` | Show session and context details |
283
336
  | `/skills` | Show available and active Agent Skills |
284
337
  | `/mcp` | Show MCP servers and runtime tools |
338
+ | `/memory` | Browse stored global memories |
285
339
  | `/compact [retire]` | Swap tool output or retire a cold history prefix |
286
340
  | `/clear` | Start a new session and clear conversation |
287
341
  | `/fork` | Clone the current session |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "A personal coding agent with an interactive TUI and one-shot CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,6 +20,7 @@
20
20
  "src/image",
21
21
  "src/instructions",
22
22
  "src/mcp",
23
+ "src/memory",
23
24
  "src/model",
24
25
  "src/observation",
25
26
  "src/session",
@@ -62,11 +63,14 @@
62
63
  "docs:check": "bun scripts/render-public-contract-docs.ts --check",
63
64
  "tinker": "bun src/cli/index.ts",
64
65
  "check": "bun run typecheck && bun run format:check && bun run lint && bun run docs:check && bun run test && bun run bench:smoke",
66
+ "check:fast": "bun run typecheck && bun run format:check && bun run lint && bun run test:fast",
65
67
  "format": "biome format --write .",
66
68
  "format:check": "biome format .",
67
69
  "lint": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --max-warnings=0",
68
70
  "lint:fix": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --fix",
69
71
  "test": "FORCE_COLOR=0 bun test",
72
+ "test:fast": "FORCE_COLOR=0 bun test --path-ignore-patterns='**/*pty*.test.ts' --path-ignore-patterns='**/native-host-integration.test.ts'",
73
+ "test:e2e": "FORCE_COLOR=0 bun test src/__tests__/cli-pty src/__tests__/pty-tui-harness packages/tinker-chrome/__tests__/native-host-integration",
70
74
  "typecheck": "tsc --noEmit"
71
75
  },
72
76
  "dependencies": {
@@ -76,6 +80,7 @@
76
80
  "@modelcontextprotocol/sdk": "^1.29.0",
77
81
  "@mozilla/readability": "^0.6.0",
78
82
  "@vscode/ripgrep": "1.18.0",
83
+ "ansi-escapes": "^7.3.0",
79
84
  "bun": "1.3.14",
80
85
  "clipboardy": "^5.3.1",
81
86
  "commander": "^14.0.3",
@@ -13,6 +13,7 @@ import {
13
13
  runtimeIdFactory,
14
14
  type RuntimeIdFactory,
15
15
  type SessionId,
16
+ type TurnId,
16
17
  } from "../ids/runtime-id";
17
18
  import { loadMcpConfig } from "../mcp/mcp-config";
18
19
  import {
@@ -71,11 +72,13 @@ import {
71
72
  renderedMessageHash,
72
73
  } from "../context/compiled-context-hash";
73
74
  import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
75
+ import type { ToolExecutor } from "../tools/types";
74
76
  import type { Refiner } from "../tools/web-fetch/refiner";
75
77
  import type { ProjectInstructionManifest } from "../instructions/project-instructions";
76
78
  import {
77
79
  SessionStore,
78
80
  createSessionCompatibilityContract,
81
+ type CompletedTurnSnapshot,
79
82
  type SessionRecoveryResult,
80
83
  type StoredSkillActivation,
81
84
  } from "../session/session-store";
@@ -212,6 +215,25 @@ export type SkillsUpdateSummary = {
212
215
  readonly addedOverrideCount: number;
213
216
  };
214
217
 
218
+ export type CompletedTurnHookInput = {
219
+ readonly workspaceRoot: string;
220
+ readonly sessionId: SessionId;
221
+ readonly turnId: TurnId;
222
+ readonly snapshot: CompletedTurnSnapshot;
223
+ };
224
+
225
+ export type CompletedTurnHookFailure = {
226
+ readonly workspaceRoot: string;
227
+ readonly sessionId: SessionId;
228
+ readonly turnId: TurnId;
229
+ readonly reason: "completed_turn_snapshot_failed" | "completed_turn_enqueue_failed";
230
+ };
231
+
232
+ export type CompletedTurnHook = {
233
+ enqueue(input: CompletedTurnHookInput): void;
234
+ recordFailure(input: CompletedTurnHookFailure): void;
235
+ };
236
+
215
237
  type CommonRuntimeSessionInput = {
216
238
  workspaceRoot: string;
217
239
  modelName: string;
@@ -233,6 +255,8 @@ type CommonRuntimeSessionInput = {
233
255
  };
234
256
  webFetchRefiner?: Refiner;
235
257
  toolingConfig?: PublicToolingConfig;
258
+ memorySearch?: ToolExecutor;
259
+ completedTurnHook?: CompletedTurnHook;
236
260
  };
237
261
 
238
262
  type CreateNewRuntimeSessionInput = CommonRuntimeSessionInput & {
@@ -534,6 +558,9 @@ class DefaultRuntimeSession implements RuntimeSession {
534
558
  historyReader: store.historyReader(),
535
559
  webFetchRefiner: input.webFetchRefiner,
536
560
  toolingConfig: input.toolingConfig,
561
+ ...(input.memorySearch === undefined
562
+ ? {}
563
+ : { memorySearch: input.memorySearch }),
537
564
  ...(session.skillCatalog.skills.size === 0
538
565
  ? {}
539
566
  : {
@@ -1717,6 +1744,9 @@ class DefaultRuntimeSession implements RuntimeSession {
1717
1744
  await this.appendTerminalEvent(turn, result, projectedMessageCount);
1718
1745
  pendingLedgerTurn.finish(result);
1719
1746
  settled = true;
1747
+ if (result.status === "completed") {
1748
+ this.notifyCompletedTurn(turn);
1749
+ }
1720
1750
  await this.settleClosedTurnSkills();
1721
1751
  if (result.status === "completed") {
1722
1752
  await this.performAutomaticContextMaintenance();
@@ -1764,6 +1794,55 @@ class DefaultRuntimeSession implements RuntimeSession {
1764
1794
  }
1765
1795
  }
1766
1796
 
1797
+ private notifyCompletedTurn(turn: TurnIdentity): void {
1798
+ const hook = this.input.completedTurnHook;
1799
+ if (hook === undefined) {
1800
+ return;
1801
+ }
1802
+ let snapshot: CompletedTurnSnapshot;
1803
+ try {
1804
+ snapshot = this.store.readCompletedTurnSnapshot(turn.turnId);
1805
+ } catch {
1806
+ this.recordCompletedTurnHookFailure(
1807
+ hook,
1808
+ turn.turnId,
1809
+ "completed_turn_snapshot_failed",
1810
+ );
1811
+ return;
1812
+ }
1813
+ try {
1814
+ hook.enqueue({
1815
+ workspaceRoot: this.input.workspaceRoot,
1816
+ sessionId: this.sessionId,
1817
+ turnId: turn.turnId,
1818
+ snapshot,
1819
+ });
1820
+ } catch {
1821
+ this.recordCompletedTurnHookFailure(
1822
+ hook,
1823
+ turn.turnId,
1824
+ "completed_turn_enqueue_failed",
1825
+ );
1826
+ }
1827
+ }
1828
+
1829
+ private recordCompletedTurnHookFailure(
1830
+ hook: CompletedTurnHook,
1831
+ turnId: TurnId,
1832
+ reason: CompletedTurnHookFailure["reason"],
1833
+ ): void {
1834
+ try {
1835
+ hook.recordFailure({
1836
+ workspaceRoot: this.input.workspaceRoot,
1837
+ sessionId: this.sessionId,
1838
+ turnId,
1839
+ reason,
1840
+ });
1841
+ } catch {
1842
+ // Optional completed-turn integrations never fault a committed turn.
1843
+ }
1844
+ }
1845
+
1767
1846
  private async performAutomaticCompaction(
1768
1847
  qualificationId: string,
1769
1848
  ): Promise<ContextCompactionResult | undefined> {
package/src/cli/config.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  type ModelTokenEstimatorProfile,
17
17
  unknownProfileError,
18
18
  } from "./model-profiles";
19
+ import type { MemoryEmbeddingConfig } from "../memory/contracts";
19
20
  import {
20
21
  parsePublicEnvironment,
21
22
  type ParsedPublicEnvironment,
@@ -45,6 +46,12 @@ export type RunnerConfigSelection = {
45
46
 
46
47
  type RunnerConfigTemplate = Omit<RunnerConfig, "sessionId">;
47
48
 
49
+ export type ResolvedMemoryConfig = {
50
+ readonly profile: ModelProfile;
51
+ readonly contextBudget: ModelContextBudget;
52
+ readonly embedding: MemoryEmbeddingConfig;
53
+ };
54
+
48
55
  export type ResolvedPublicConfig =
49
56
  | {
50
57
  readonly mode: "env";
@@ -56,6 +63,7 @@ export type ResolvedPublicConfig =
56
63
  readonly tooling: PublicToolingConfig;
57
64
  readonly profiles: ModelProfiles;
58
65
  readonly templates: ReadonlyMap<string, RunnerConfigTemplate>;
66
+ readonly memory?: ResolvedMemoryConfig;
59
67
  readonly persistDefaultProfile: (profileName: string) => Promise<void>;
60
68
  };
61
69
 
@@ -85,11 +93,16 @@ export function createResolvedPublicConfig(
85
93
  runnerConfigTemplateFromProfile(environment, profile),
86
94
  ]),
87
95
  );
96
+ const memory =
97
+ profiles.memory === undefined
98
+ ? undefined
99
+ : resolveMemoryConfig(profiles, profiles.memory.profile);
88
100
  return Object.freeze({
89
101
  mode: "profile",
90
102
  tooling: environment.tooling,
91
103
  profiles,
92
104
  templates,
105
+ ...(memory === undefined ? {} : { memory }),
93
106
  persistDefaultProfile: (profileName: string) =>
94
107
  persistDefaultProfile(profileName, environment.modelsPath),
95
108
  });
@@ -105,6 +118,22 @@ export function createResolvedPublicConfig(
105
118
  });
106
119
  }
107
120
 
121
+ function resolveMemoryConfig(
122
+ profiles: ModelProfiles,
123
+ profileName: string,
124
+ ): ResolvedMemoryConfig {
125
+ const profile = profiles.profiles.get(profileName);
126
+ if (profile === undefined || profiles.memory === undefined) {
127
+ throw unknownProfileError(profileName, profiles);
128
+ }
129
+ const contextProfile = profileToContextProfile(profile);
130
+ return Object.freeze({
131
+ profile,
132
+ contextBudget: deriveModelContextBudget(contextProfile),
133
+ embedding: profiles.memory.embedding,
134
+ });
135
+ }
136
+
108
137
  export function deriveRunnerConfig(
109
138
  snapshot: ResolvedPublicConfig,
110
139
  selection: RunnerConfigSelection,
@@ -4,12 +4,15 @@ import {
4
4
  type ModelContextProfile,
5
5
  } from "../model/model-context-profile";
6
6
  import {
7
+ MEMORY_CONFIG_FIELDS,
8
+ MEMORY_EMBEDDING_FIELDS,
7
9
  MODEL_PROFILE_FIELDS,
8
10
  MODEL_PROFILES_DOCUMENT_FIELDS,
9
11
  MODEL_TOKEN_ESTIMATOR_FIELDS,
10
12
  type ModelTokenEstimatorKind,
11
13
  type ModelTokenEstimatorMaxRetries,
12
14
  } from "./public-config-contract";
15
+ import type { MemoryEmbeddingConfig } from "../memory/contracts";
13
16
 
14
17
  export type ModelProfile = {
15
18
  readonly name: string;
@@ -38,6 +41,12 @@ export type ModelTokenEstimatorProfile = {
38
41
  export type ModelProfiles = {
39
42
  readonly defaultProfile: string;
40
43
  readonly profiles: ReadonlyMap<string, ModelProfile>;
44
+ readonly memory?: MemoryConfig;
45
+ };
46
+
47
+ export type MemoryConfig = {
48
+ readonly profile: string;
49
+ readonly embedding: MemoryEmbeddingConfig;
41
50
  };
42
51
 
43
52
  export async function loadModelProfiles(configPath: string): Promise<ModelProfiles> {
@@ -144,7 +153,15 @@ export function parseModelProfiles(raw: string, sourcePath: string): ModelProfil
144
153
  profiles.set(profileName, parseProfile(profileName, profileValue, sourcePath));
145
154
  }
146
155
 
147
- return { defaultProfile, profiles };
156
+ const memory =
157
+ json.memory === undefined
158
+ ? undefined
159
+ : parseMemoryConfig(json.memory, profiles, sourcePath);
160
+ return Object.freeze({
161
+ defaultProfile,
162
+ profiles,
163
+ ...(memory === undefined ? {} : { memory }),
164
+ });
148
165
  }
149
166
 
150
167
  export function resolveModelProfile(
@@ -286,6 +303,56 @@ function parseProfile(
286
303
  });
287
304
  }
288
305
 
306
+ function parseMemoryConfig(
307
+ value: unknown,
308
+ profiles: ReadonlyMap<string, ModelProfile>,
309
+ sourcePath: string,
310
+ ): MemoryConfig {
311
+ const where = `Model profiles ${sourcePath}: "memory"`;
312
+ if (!isRecord(value)) {
313
+ throw new Error(`${where} must be an object.`);
314
+ }
315
+ assertKnownKeys(
316
+ value,
317
+ MEMORY_CONFIG_FIELDS.map((field) => field.name),
318
+ where,
319
+ );
320
+ const profile = requireString(value.profile, `${where}.profile`);
321
+ if (!profiles.has(profile)) {
322
+ throw unknownProfileNamesError(profile, [...profiles.keys()]);
323
+ }
324
+ const embedding = parseMemoryEmbedding(value.embedding, `${where}.embedding`);
325
+ return Object.freeze({ profile, embedding });
326
+ }
327
+
328
+ function parseMemoryEmbedding(value: unknown, where: string): MemoryEmbeddingConfig {
329
+ if (!isRecord(value)) {
330
+ throw new Error(`${where} must be an object.`);
331
+ }
332
+ assertKnownKeys(
333
+ value,
334
+ MEMORY_EMBEDDING_FIELDS.map((field) => field.name),
335
+ where,
336
+ );
337
+ const name = requireString(value.name, `${where}.name`);
338
+ if (value.kind !== "openai-compatible") {
339
+ throw new Error(`${where}.kind must be "openai-compatible".`);
340
+ }
341
+ const model = requireString(value.model, `${where}.model`);
342
+ const apiBase = requireString(value.apiBase, `${where}.apiBase`);
343
+ requireHttpUrl(apiBase, `${where}.apiBase`);
344
+ const apiKey = requireString(value.apiKey, `${where}.apiKey`);
345
+ const dimensions = requirePositiveInteger(value.dimensions, `${where}.dimensions`);
346
+ return Object.freeze({
347
+ name,
348
+ kind: "openai-compatible",
349
+ model,
350
+ apiBase,
351
+ apiKey,
352
+ dimensions,
353
+ });
354
+ }
355
+
289
356
  function parseInputModalities(
290
357
  value: unknown,
291
358
  name: string,
@@ -472,6 +539,22 @@ function requirePositiveInteger(value: unknown, name: string): number {
472
539
  return value;
473
540
  }
474
541
 
542
+ function requireHttpUrl(value: string, name: string): void {
543
+ let parsed: URL;
544
+ try {
545
+ parsed = new URL(value);
546
+ } catch {
547
+ throw new Error(`${name} must be a valid HTTP(S) URL.`);
548
+ }
549
+ if (
550
+ (parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
551
+ parsed.username !== "" ||
552
+ parsed.password !== ""
553
+ ) {
554
+ throw new Error(`${name} must be a valid HTTP(S) URL.`);
555
+ }
556
+ }
557
+
475
558
  function parseBoolean(value: unknown, name: string): boolean {
476
559
  if (typeof value === "boolean") {
477
560
  return value;
@@ -376,9 +376,91 @@ export type ModelTokenEstimatorMaxRetries = Extract<
376
376
  { readonly name: "maxRetries" }
377
377
  >["literalValue"];
378
378
 
379
+ export type MemoryConfigField = {
380
+ readonly name: "profile" | "embedding";
381
+ readonly valueKind: "non-empty-string" | "embedding-profile";
382
+ readonly required: true;
383
+ readonly secret: boolean;
384
+ readonly description: string;
385
+ };
386
+
387
+ export const MEMORY_CONFIG_FIELDS = Object.freeze([
388
+ Object.freeze({
389
+ name: "profile",
390
+ valueKind: "non-empty-string",
391
+ required: true,
392
+ secret: false,
393
+ description:
394
+ "Existing model profile used for completed-turn atomic-memory extraction.",
395
+ }),
396
+ Object.freeze({
397
+ name: "embedding",
398
+ valueKind: "embedding-profile",
399
+ required: true,
400
+ secret: true,
401
+ description: "Single embedding profile for the global memory database.",
402
+ }),
403
+ ] satisfies readonly MemoryConfigField[]);
404
+
405
+ export type MemoryEmbeddingField = {
406
+ readonly name: "name" | "kind" | "model" | "apiBase" | "apiKey" | "dimensions";
407
+ readonly valueKind: "non-empty-string" | "positive-integer" | "literal-string";
408
+ readonly required: true;
409
+ readonly secret: boolean;
410
+ readonly literalValue?: "openai-compatible";
411
+ readonly description: string;
412
+ };
413
+
414
+ export const MEMORY_EMBEDDING_FIELDS = Object.freeze([
415
+ Object.freeze({
416
+ name: "name",
417
+ valueKind: "non-empty-string",
418
+ required: true,
419
+ secret: false,
420
+ description: "Stable identity for the embedding space.",
421
+ }),
422
+ Object.freeze({
423
+ name: "kind",
424
+ valueKind: "literal-string",
425
+ required: true,
426
+ secret: false,
427
+ literalValue: "openai-compatible",
428
+ description: "Embedding transport kind.",
429
+ }),
430
+ Object.freeze({
431
+ name: "model",
432
+ valueKind: "non-empty-string",
433
+ required: true,
434
+ secret: false,
435
+ description: "Embedding provider model name.",
436
+ }),
437
+ Object.freeze({
438
+ name: "apiBase",
439
+ valueKind: "non-empty-string",
440
+ required: true,
441
+ secret: false,
442
+ description: "OpenAI-compatible API base URL.",
443
+ }),
444
+ Object.freeze({
445
+ name: "apiKey",
446
+ valueKind: "non-empty-string",
447
+ required: true,
448
+ secret: true,
449
+ description: "Embedding provider credential.",
450
+ }),
451
+ Object.freeze({
452
+ name: "dimensions",
453
+ valueKind: "positive-integer",
454
+ required: true,
455
+ secret: false,
456
+ description: "Fixed vector dimensions for the global memory database.",
457
+ }),
458
+ ] satisfies readonly MemoryEmbeddingField[]);
459
+
379
460
  export const MODEL_PROFILES_DOCUMENT_FIELDS = Object.freeze([
380
461
  Object.freeze({ name: "default", valueKind: "non-empty-string" as const }),
381
462
  Object.freeze({ name: "profiles", valueKind: "profiles-map" as const }),
463
+ Object.freeze({ name: "memory", valueKind: "memory-config" as const }),
382
464
  ]);
383
465
 
384
466
  export type PublicToolingConfig = {
@@ -64,6 +64,14 @@ export function createModelClient(
64
64
  return new FakeModelClient(fakeMode, {
65
65
  model: config.modelName,
66
66
  contextBudget: config.contextBudget,
67
+ inputModalities: config.inputModalities,
68
+ ...(config.tokenEstimator === undefined
69
+ ? {}
70
+ : { tokenEstimator: config.tokenEstimator }),
71
+ ...(env.TINKER_TEST_FAKE_MODEL_REQUEST_LOG === undefined ||
72
+ env.TINKER_TEST_FAKE_MODEL_REQUEST_LOG === ""
73
+ ? {}
74
+ : { requestLogPath: env.TINKER_TEST_FAKE_MODEL_REQUEST_LOG }),
67
75
  });
68
76
  }
69
77
 
@@ -0,0 +1,67 @@
1
+ import {
2
+ boundedMemoryError,
3
+ memoryErrorCode,
4
+ type MemoryPaths,
5
+ } from "../memory/contracts";
6
+ import { MemoryCoordinator } from "../memory/memory-coordinator";
7
+ import { MemoryLog } from "../memory/memory-log";
8
+ import { resolveMemoryPaths } from "../memory/memory-store";
9
+ import type { ResolvedMemoryConfig } from "./config";
10
+ import { createModelClient } from "./runner-dependencies";
11
+
12
+ export type TuiMemoryInitialization = {
13
+ readonly coordinator?: MemoryCoordinator;
14
+ readonly notice?: string;
15
+ };
16
+
17
+ export async function initializeTuiMemory(input: {
18
+ readonly config?: ResolvedMemoryConfig;
19
+ readonly env: NodeJS.ProcessEnv;
20
+ readonly paths?: MemoryPaths;
21
+ readonly createCoordinator?: typeof MemoryCoordinator.create;
22
+ }): Promise<TuiMemoryInitialization> {
23
+ if (input.config === undefined) {
24
+ return Object.freeze({});
25
+ }
26
+
27
+ const paths = input.paths ?? resolveMemoryPaths();
28
+ const log = new MemoryLog(paths.log);
29
+ try {
30
+ const memoryConfig = input.config;
31
+ const coordinator = await (input.createCoordinator ?? MemoryCoordinator.create)({
32
+ paths,
33
+ embedding: memoryConfig.embedding,
34
+ extractionContextBudget: memoryConfig.contextBudget,
35
+ createExtractionClient: () => {
36
+ const profile = memoryConfig.profile;
37
+ return createModelClient(
38
+ {
39
+ modelName: profile.model,
40
+ apiKey: profile.apiKey,
41
+ apiBase: profile.apiBase,
42
+ includeReasoningContent: profile.includeReasoningContent,
43
+ stream: profile.stream,
44
+ contextBudget: memoryConfig.contextBudget,
45
+ inputModalities: profile.inputModalities,
46
+ ...(profile.tokenEstimator === undefined
47
+ ? {}
48
+ : { tokenEstimator: profile.tokenEstimator }),
49
+ },
50
+ input.env,
51
+ );
52
+ },
53
+ });
54
+ return Object.freeze({ coordinator });
55
+ } catch (error) {
56
+ const reason = memoryErrorCode(error, "memory_init_failed");
57
+ await log.append({
58
+ at: new Date().toISOString(),
59
+ kind: "init",
60
+ outcome: "failed",
61
+ reason,
62
+ });
63
+ return Object.freeze({
64
+ notice: `memory disabled: ${boundedMemoryError(error)}`,
65
+ });
66
+ }
67
+ }