tinker-agent 1.4.0 → 1.5.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.
- package/CHANGELOG.md +23 -1
- package/README.md +54 -0
- package/package.json +5 -1
- package/src/agent/runtime-session.ts +79 -0
- package/src/cli/config.ts +29 -0
- package/src/cli/model-profiles.ts +84 -1
- package/src/cli/public-config-contract.ts +82 -0
- package/src/cli/runner-dependencies.ts +8 -0
- package/src/cli/tui-memory.ts +67 -0
- package/src/cli/tui-runner.tsx +27 -0
- package/src/context/context-policy.ts +2 -2
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/memory/contracts.ts +148 -0
- package/src/memory/embedding-client.ts +105 -0
- package/src/memory/memory-coordinator.ts +556 -0
- package/src/memory/memory-extractor.ts +231 -0
- package/src/memory/memory-log.ts +88 -0
- package/src/memory/memory-search-tool.ts +100 -0
- package/src/memory/memory-store.ts +687 -0
- package/src/memory/vector.ts +153 -0
- package/src/model/fake-model-client.ts +971 -3
- package/src/observation/observation-builder.ts +20 -0
- package/src/session/session-store.ts +123 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/types.ts +16 -0
- package/src/tui/app.tsx +69 -2
- package/src/tui/clipboard.ts +22 -0
- package/src/tui/components/footer.tsx +9 -4
- package/src/tui/components/memory-browser.tsx +151 -0
- package/src/tui/event-store.ts +9 -2
- package/src/tui/slash-commands.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-07-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add optional global Memory for the interactive TUI. When configured, Tinker
|
|
13
|
+
extracts durable facts from completed turns, stores them locally, and exposes
|
|
14
|
+
relevant memories to the agent through `MemorySearch`.
|
|
15
|
+
- Add a read-only `/memory` browser for reviewing the memories stored in the
|
|
16
|
+
current global Memory database.
|
|
17
|
+
- Show live elapsed time in the TUI footer while a turn is running.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Compact and retire context toward 30% input utilization instead of 60%, leaving
|
|
22
|
+
more headroom for continued work after automatic or manual maintenance.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Let the Tinker Chrome MCP server exit promptly when its client closes stdin,
|
|
27
|
+
avoiding the shutdown timeout during ordinary CLI exit.
|
|
28
|
+
|
|
8
29
|
## [1.4.0] - 2026-07-24
|
|
9
30
|
|
|
10
31
|
### Added
|
|
@@ -63,7 +84,8 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
63
84
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
64
85
|
executable.
|
|
65
86
|
|
|
66
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.
|
|
87
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.5.0...HEAD
|
|
88
|
+
[1.5.0]: https://github.com/ishowshao/tinker/releases/tag/v1.5.0
|
|
67
89
|
[1.4.0]: https://github.com/ishowshao/tinker/releases/tag/v1.4.0
|
|
68
90
|
[1.3.0]: https://github.com/ishowshao/tinker/releases/tag/v1.3.0
|
|
69
91
|
[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.
|
|
3
|
+
"version": "1.5.0",
|
|
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": {
|
|
@@ -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
|
-
|
|
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
|
+
}
|