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.
@@ -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: { toolChoice: "auto" },
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: { store: false, toolChoice: "auto" },
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 { decodeStoredToolCalls, decodeStoredToolRawResult } from "./session-store";
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 = path.join(
58
+ const databasePath = await resolveSessionDatabasePath(
55
59
  workspaceRoot,
56
- ".tinker",
57
- "sessions",
58
60
  input.sessionId,
59
- "session.sqlite",
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(private readonly input: { workspaceRoot: string; limit?: number }) {
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 = path.join(workspaceRoot, ".tinker", "sessions");
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(workspaceRoot, ".tinker", "sessions", sessionId);
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, sessionDatabasePath } from "./session-store";
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(sessionDatabasePath(workspaceRoot, input.sessionId), {
19
- readonly: true,
20
- strict: true,
21
- safeIntegers: true,
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
  }