tinker-agent 2.2.0 → 2.4.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 +36 -1
- package/README.md +18 -8
- package/package.json +1 -1
- package/src/agent/loop.ts +1 -1
- package/src/agent/runtime-session.ts +279 -28
- package/src/agent/session-ledger.ts +10 -3
- package/src/cli/config.ts +8 -12
- package/src/cli/tui-runner.tsx +3 -1
- package/src/context/context-manager.ts +185 -8
- package/src/context/context-policy.ts +1 -1
- package/src/context/context-revision-compiler.ts +7 -2
- package/src/context/context-swap-label.ts +132 -0
- package/src/context/swap-planner.ts +219 -56
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/events/types.ts +3 -3
- package/src/image/image-asset-store.ts +21 -8
- package/src/memory/contracts.ts +31 -0
- package/src/memory/memory-coordinator.ts +7 -0
- package/src/memory/memory-extractor.ts +26 -5
- package/src/model/fake-model-client.ts +7 -0
- package/src/model/model-client.ts +11 -0
- package/src/model/openai-chat-model-client.ts +7 -1
- package/src/model/openai-responses-model-client.ts +8 -1
- package/src/observation/observation-builder.ts +36 -0
- package/src/session/resume-projection.ts +8 -6
- package/src/session/session-catalog.ts +18 -3
- package/src/session/session-last-response-reader.ts +10 -6
- package/src/session/session-store.ts +340 -8
- package/src/session/workspace-storage.ts +84 -0
- package/src/tools/bash-task.ts +14 -12
- package/src/tools/context-maintenance.ts +266 -0
- package/src/tools/registry.ts +20 -1
- package/src/tools/types.ts +83 -1
- package/src/tui/event-store.ts +15 -0
|
@@ -86,6 +86,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
86
86
|
requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
87
87
|
inputModalities: this.inputModalities,
|
|
88
88
|
toolResultModalities: this.toolResultModalities,
|
|
89
|
+
responseFormat: input.responseFormat?.type ?? null,
|
|
89
90
|
}),
|
|
90
91
|
);
|
|
91
92
|
const prepared: PreparedModelRequest = Object.freeze({
|
|
@@ -96,6 +97,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
96
97
|
tools: Object.freeze([...input.tools]),
|
|
97
98
|
maxTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
98
99
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
100
|
+
...(input.responseFormat === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { responseFormat: input.responseFormat }),
|
|
99
103
|
}),
|
|
100
104
|
promptSegments: Object.freeze([...toolSegments, ...messageSegments]),
|
|
101
105
|
requestConfigHash,
|
|
@@ -111,6 +115,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
111
115
|
this.preparedInputs.set(prepared, {
|
|
112
116
|
messages: [...input.messages],
|
|
113
117
|
tools: [...input.tools],
|
|
118
|
+
...(input.responseFormat === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: { responseFormat: input.responseFormat }),
|
|
114
121
|
});
|
|
115
122
|
return prepared;
|
|
116
123
|
}
|
|
@@ -103,9 +103,20 @@ export type ModelRequestOptions = {
|
|
|
103
103
|
};
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Provider-enforced structured output for a single request. Distinct from
|
|
108
|
+
* prompt-level instructions: the provider constrains the decoded response to
|
|
109
|
+
* be a valid JSON object, so callers still validate shape but no longer fight
|
|
110
|
+
* markdown fences or surrounding prose.
|
|
111
|
+
*/
|
|
112
|
+
export type ModelResponseFormat = {
|
|
113
|
+
readonly type: "json_object";
|
|
114
|
+
};
|
|
115
|
+
|
|
106
116
|
export type ModelRequestInput = {
|
|
107
117
|
messages: AgentMessage[];
|
|
108
118
|
tools: ToolDefinition[];
|
|
119
|
+
responseFormat?: ModelResponseFormat;
|
|
109
120
|
};
|
|
110
121
|
|
|
111
122
|
export type PreparedPromptSegmentKind =
|
|
@@ -107,6 +107,9 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
107
107
|
messages,
|
|
108
108
|
...(tools === undefined ? {} : { tools, tool_choice: "auto" as const }),
|
|
109
109
|
...(reasoningEffort === undefined ? {} : { reasoning_effort: reasoningEffort }),
|
|
110
|
+
...(input.responseFormat === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { response_format: { type: input.responseFormat.type } }),
|
|
110
113
|
max_completion_tokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
111
114
|
...(this.stream
|
|
112
115
|
? {
|
|
@@ -147,7 +150,10 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
147
150
|
stream: this.stream,
|
|
148
151
|
inputModalities: this.inputModalities,
|
|
149
152
|
toolResultModalities: this.toolResultModalities,
|
|
150
|
-
requestPolicy: {
|
|
153
|
+
requestPolicy: {
|
|
154
|
+
toolChoice: "auto",
|
|
155
|
+
responseFormat: input.responseFormat?.type ?? null,
|
|
156
|
+
},
|
|
151
157
|
imagePolicy: {
|
|
152
158
|
version: IMAGE_INPUT_POLICY_VERSION,
|
|
153
159
|
...IMAGE_INPUT_POLICY,
|
|
@@ -111,6 +111,9 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
111
111
|
: { reasoning: { effort: reasoningEffort } }),
|
|
112
112
|
max_output_tokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
113
113
|
store: false as const,
|
|
114
|
+
...(input.responseFormat === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { text: { format: { type: input.responseFormat.type } } }),
|
|
114
117
|
...(this.stream ? { stream: true as const } : {}),
|
|
115
118
|
});
|
|
116
119
|
const toolSegments = (tools ?? []).map(
|
|
@@ -148,7 +151,11 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
148
151
|
stream: this.stream,
|
|
149
152
|
inputModalities: this.inputModalities,
|
|
150
153
|
toolResultModalities: this.toolResultModalities,
|
|
151
|
-
requestPolicy: {
|
|
154
|
+
requestPolicy: {
|
|
155
|
+
store: false,
|
|
156
|
+
toolChoice: "auto",
|
|
157
|
+
responseFormat: input.responseFormat?.type ?? null,
|
|
158
|
+
},
|
|
152
159
|
imagePolicy: {
|
|
153
160
|
version: IMAGE_INPUT_POLICY_VERSION,
|
|
154
161
|
...IMAGE_INPUT_POLICY,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../agent/tool-result-content";
|
|
7
7
|
import type {
|
|
8
8
|
BashRawResult,
|
|
9
|
+
ContextMaintenanceRawResult,
|
|
9
10
|
DeleteFileRawResult,
|
|
10
11
|
EditFileRawResult,
|
|
11
12
|
GenericToolRawResult,
|
|
@@ -48,6 +49,8 @@ export class ObservationBuilder {
|
|
|
48
49
|
return renderViewImageObservation(input.raw);
|
|
49
50
|
case "recall":
|
|
50
51
|
return textObservation(renderRecallObservation(input.raw));
|
|
52
|
+
case "context_maintenance":
|
|
53
|
+
return textObservation(renderContextMaintenanceObservation(input.raw));
|
|
51
54
|
case "memory_search":
|
|
52
55
|
return textObservation(renderMemorySearchObservation(input.raw));
|
|
53
56
|
case "memory_get":
|
|
@@ -117,6 +120,39 @@ function renderWaitObservation(raw: WaitRawResult): string {
|
|
|
117
120
|
: `Wait failed: ${raw.error}`;
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function renderContextMaintenanceObservation(raw: ContextMaintenanceRawResult): string {
|
|
124
|
+
if (!raw.ok) {
|
|
125
|
+
if (raw.operation === "swap" && raw.rejected.length > 0) {
|
|
126
|
+
return JSON.stringify({ ok: false, rejected: raw.rejected });
|
|
127
|
+
}
|
|
128
|
+
return JSON.stringify({ ok: false, error: raw.error });
|
|
129
|
+
}
|
|
130
|
+
switch (raw.operation) {
|
|
131
|
+
case "status":
|
|
132
|
+
return JSON.stringify({
|
|
133
|
+
ok: true,
|
|
134
|
+
usedInputTokens: raw.usedInputTokens,
|
|
135
|
+
inputBudgetTokens: raw.inputBudgetTokens,
|
|
136
|
+
pressure: raw.pressure,
|
|
137
|
+
triggerTokens: raw.triggerTokens,
|
|
138
|
+
source: raw.source,
|
|
139
|
+
});
|
|
140
|
+
case "candidates":
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
ok: true,
|
|
143
|
+
total: raw.total,
|
|
144
|
+
candidates: raw.candidates,
|
|
145
|
+
});
|
|
146
|
+
case "swap":
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
ok: true,
|
|
149
|
+
scheduled: raw.scheduled,
|
|
150
|
+
rejected: raw.rejected,
|
|
151
|
+
note: raw.note,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
function assertNever(value: never): never {
|
|
121
157
|
throw new Error(`Unhandled tool raw result: ${JSON.stringify(value)}`);
|
|
122
158
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { realpath } from "node:fs/promises";
|
|
3
2
|
import { Database } from "bun:sqlite";
|
|
4
3
|
import type { ToolCall } from "../agent/types";
|
|
@@ -24,7 +23,11 @@ import {
|
|
|
24
23
|
} from "../tui/tui-projection-policy";
|
|
25
24
|
import { SessionError } from "./session-errors";
|
|
26
25
|
import { verifySessionSchema } from "./session-schema";
|
|
27
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
decodeStoredToolCalls,
|
|
28
|
+
decodeStoredToolRawResult,
|
|
29
|
+
resolveSessionDatabasePath,
|
|
30
|
+
} from "./session-store";
|
|
28
31
|
import {
|
|
29
32
|
MAX_TIMELINE_PROMPT_CODE_POINTS,
|
|
30
33
|
projectUserMessage,
|
|
@@ -46,17 +49,16 @@ export class ResumeProjectionReader {
|
|
|
46
49
|
sessionId: SessionId;
|
|
47
50
|
modelName: string;
|
|
48
51
|
policy?: TuiProjectionPolicy;
|
|
52
|
+
homeRoot?: string;
|
|
49
53
|
}): Promise<TuiProjectionState> {
|
|
50
54
|
const policy = validateTuiProjectionPolicy(
|
|
51
55
|
input.policy ?? defaultTuiProjectionPolicy,
|
|
52
56
|
);
|
|
53
57
|
const workspaceRoot = await realpath(input.workspaceRoot);
|
|
54
|
-
const databasePath =
|
|
58
|
+
const databasePath = await resolveSessionDatabasePath(
|
|
55
59
|
workspaceRoot,
|
|
56
|
-
".tinker",
|
|
57
|
-
"sessions",
|
|
58
60
|
input.sessionId,
|
|
59
|
-
|
|
61
|
+
input.homeRoot,
|
|
60
62
|
);
|
|
61
63
|
const database = new Database(databasePath, {
|
|
62
64
|
readonly: true,
|
|
@@ -6,6 +6,7 @@ import { SessionError } from "./session-errors";
|
|
|
6
6
|
import { inspectSessionLock } from "./session-lock";
|
|
7
7
|
import { SessionStore } from "./session-store";
|
|
8
8
|
import { verifyReadableSessionSchema } from "./session-schema";
|
|
9
|
+
import { canonicalHomeRoot, workspaceStorageRoot } from "./workspace-storage";
|
|
9
10
|
|
|
10
11
|
export type SessionSummary = {
|
|
11
12
|
sessionId: SessionId;
|
|
@@ -28,9 +29,22 @@ export type SessionSummary = {
|
|
|
28
29
|
|
|
29
30
|
export class SessionCatalog {
|
|
30
31
|
private readonly workspaceRootPromise: Promise<string>;
|
|
32
|
+
private readonly sessionsRootPromise: Promise<string>;
|
|
31
33
|
|
|
32
|
-
constructor(
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly input: {
|
|
36
|
+
workspaceRoot: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
homeRoot?: string;
|
|
39
|
+
},
|
|
40
|
+
) {
|
|
33
41
|
this.workspaceRootPromise = realpath(input.workspaceRoot);
|
|
42
|
+
this.sessionsRootPromise = this.workspaceRootPromise.then(async (workspaceRoot) =>
|
|
43
|
+
path.join(
|
|
44
|
+
workspaceStorageRoot(workspaceRoot, await canonicalHomeRoot(input.homeRoot)),
|
|
45
|
+
"sessions",
|
|
46
|
+
),
|
|
47
|
+
);
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
async list(currentSessionId?: SessionId): Promise<readonly SessionSummary[]> {
|
|
@@ -44,7 +58,7 @@ export class SessionCatalog {
|
|
|
44
58
|
|
|
45
59
|
private async scan(currentSessionId?: SessionId): Promise<SessionSummary[]> {
|
|
46
60
|
const workspaceRoot = await this.workspaceRootPromise;
|
|
47
|
-
const sessionsRoot =
|
|
61
|
+
const sessionsRoot = await this.sessionsRootPromise;
|
|
48
62
|
let entries;
|
|
49
63
|
try {
|
|
50
64
|
entries = await readdir(sessionsRoot, { withFileTypes: true });
|
|
@@ -87,7 +101,7 @@ export class SessionCatalog {
|
|
|
87
101
|
currentSessionId?: SessionId,
|
|
88
102
|
): Promise<SessionSummary> {
|
|
89
103
|
const workspaceRoot = await this.workspaceRootPromise;
|
|
90
|
-
const directory = path.join(
|
|
104
|
+
const directory = path.join(await this.sessionsRootPromise, sessionId);
|
|
91
105
|
return readSummary(directory, sessionId, workspaceRoot, currentSessionId);
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -105,6 +119,7 @@ export class SessionCatalog {
|
|
|
105
119
|
workspaceRoot,
|
|
106
120
|
sessionId,
|
|
107
121
|
allowIncomplete: true,
|
|
122
|
+
...(this.input.homeRoot === undefined ? {} : { homeRoot: this.input.homeRoot }),
|
|
108
123
|
});
|
|
109
124
|
try {
|
|
110
125
|
await store.deleteFromDisk();
|
|
@@ -4,22 +4,26 @@ import { contentHash } from "../context/protocol-frame";
|
|
|
4
4
|
import type { SessionId } from "../ids/runtime-id";
|
|
5
5
|
import { SessionError, sessionOpenError, sessionReadError } from "./session-errors";
|
|
6
6
|
import { verifySessionSchema } from "./session-schema";
|
|
7
|
-
import { decodeStoredToolCalls,
|
|
7
|
+
import { decodeStoredToolCalls, resolveSessionDatabasePath } from "./session-store";
|
|
8
8
|
|
|
9
9
|
const OPERATION = "read_last_assistant_response";
|
|
10
10
|
|
|
11
11
|
export async function readLastAssistantResponse(input: {
|
|
12
12
|
workspaceRoot: string;
|
|
13
13
|
sessionId: SessionId;
|
|
14
|
+
homeRoot?: string;
|
|
14
15
|
}): Promise<string | undefined> {
|
|
15
16
|
const workspaceRoot = await realpath(input.workspaceRoot);
|
|
16
17
|
let database: Database;
|
|
17
18
|
try {
|
|
18
|
-
database = new Database(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
database = new Database(
|
|
20
|
+
await resolveSessionDatabasePath(workspaceRoot, input.sessionId, input.homeRoot),
|
|
21
|
+
{
|
|
22
|
+
readonly: true,
|
|
23
|
+
strict: true,
|
|
24
|
+
safeIntegers: true,
|
|
25
|
+
},
|
|
26
|
+
);
|
|
23
27
|
} catch (error) {
|
|
24
28
|
throw sessionOpenError(OPERATION, input.sessionId, error);
|
|
25
29
|
}
|