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
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import type { UserMessage } from "../agent/types";
2
+ import type { AgentMessage, UserMessage } from "../agent/types";
3
3
  import {
4
4
  IMAGE_INPUT_POLICY,
5
5
  imagePlanningTokens,
@@ -13,7 +13,7 @@ import {
13
13
  type MaterializedModelRequest,
14
14
  type ModelMaterializeOptions,
15
15
  type ModelRequestInput,
16
- type PreparedMediaDescriptor,
16
+ type PreparedMediaOccurrence,
17
17
  type PreparedModelRequest,
18
18
  type PreparedPromptSegment,
19
19
  type ProviderResponseErrorCode,
@@ -124,22 +124,30 @@ export function exactJsonBodyBytes(
124
124
  );
125
125
  }
126
126
 
127
- export function imageUserSegment(message: UserMessage): PreparedPromptSegment {
128
- const media = message.attachments!.map((attachment): PreparedMediaDescriptor => {
129
- const dimensions = providerImageDimensions(attachment.width, attachment.height);
130
- return Object.freeze({
131
- assetId: attachment.assetId,
132
- label: attachment.label,
133
- range: Object.freeze({ ...attachment.range }),
134
- mimeType: attachment.mimeType,
135
- byteLength: attachment.byteLength,
136
- sourceWidth: attachment.width,
137
- sourceHeight: attachment.height,
138
- width: dimensions.width,
139
- height: dimensions.height,
140
- planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
141
- });
142
- });
127
+ export function imageUserSegment(
128
+ message: UserMessage,
129
+ messageOrdinal: number,
130
+ ): PreparedPromptSegment {
131
+ const media = message.attachments!.map(
132
+ (attachment, blockPosition): PreparedMediaOccurrence => {
133
+ const dimensions = providerImageDimensions(attachment.width, attachment.height);
134
+ return Object.freeze({
135
+ asset: Object.freeze({
136
+ assetId: attachment.assetId,
137
+ mimeType: attachment.mimeType,
138
+ byteLength: attachment.byteLength,
139
+ width: attachment.width,
140
+ height: attachment.height,
141
+ }),
142
+ source: "user_attachment",
143
+ messageOrdinal,
144
+ blockPosition,
145
+ width: dimensions.width,
146
+ height: dimensions.height,
147
+ planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
148
+ });
149
+ },
150
+ );
143
151
  return Object.freeze({
144
152
  kind: "user",
145
153
  normalizedText: message.content,
@@ -147,6 +155,35 @@ export function imageUserSegment(message: UserMessage): PreparedPromptSegment {
147
155
  });
148
156
  }
149
157
 
158
+ export function imageToolSegment(
159
+ message: Extract<AgentMessage, { role: "tool" }>,
160
+ messageOrdinal: number,
161
+ normalizedText: string,
162
+ ): PreparedPromptSegment {
163
+ const media = message.content.flatMap((block, blockPosition) => {
164
+ if (block.type !== "image") {
165
+ return [];
166
+ }
167
+ const dimensions = providerImageDimensions(block.asset.width, block.asset.height);
168
+ return [
169
+ Object.freeze<PreparedMediaOccurrence>({
170
+ asset: Object.freeze({ ...block.asset }),
171
+ source: "tool_result",
172
+ messageOrdinal,
173
+ blockPosition,
174
+ width: dimensions.width,
175
+ height: dimensions.height,
176
+ planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
177
+ }),
178
+ ];
179
+ });
180
+ return Object.freeze({
181
+ kind: "tool",
182
+ normalizedText,
183
+ ...(media.length === 0 ? {} : { media: Object.freeze(media) }),
184
+ });
185
+ }
186
+
150
187
  export function normalizedEndpointPolicy(baseURL: string | undefined): string {
151
188
  const url = new URL(baseURL ?? "https://api.openai.com/v1");
152
189
  url.username = "";
@@ -208,21 +245,15 @@ function distinctPreparedAssets(
208
245
  const assets = new Map<ImageAssetId, ImageAssetRef>();
209
246
  for (const segment of segments) {
210
247
  for (const media of segment.media ?? []) {
211
- const asset = Object.freeze({
212
- assetId: media.assetId,
213
- mimeType: media.mimeType,
214
- byteLength: media.byteLength,
215
- width: media.sourceWidth,
216
- height: media.sourceHeight,
217
- });
218
- const existing = assets.get(media.assetId);
248
+ const asset = media.asset;
249
+ const existing = assets.get(asset.assetId);
219
250
  if (
220
251
  existing !== undefined &&
221
252
  stableJsonStringify(existing) !== stableJsonStringify(asset)
222
253
  ) {
223
- throw new Error(`Conflicting descriptors for image ${media.assetId}.`);
254
+ throw new Error(`Conflicting descriptors for image ${asset.assetId}.`);
224
255
  }
225
- assets.set(media.assetId, asset);
256
+ assets.set(asset.assetId, asset);
226
257
  }
227
258
  }
228
259
  return assets;
@@ -296,10 +327,10 @@ function materializedPromptSegments(
296
327
  ...segment,
297
328
  media: Object.freeze(
298
329
  segment.media.map((media) => {
299
- const image = images.get(media.assetId);
330
+ const image = images.get(media.asset.assetId);
300
331
  if (image === undefined) {
301
332
  throw new Error(
302
- `Image ${media.assetId.slice(0, 12)}… was not materialized.`,
333
+ `Image ${media.asset.assetId.slice(0, 12)}… was not materialized.`,
303
334
  );
304
335
  }
305
336
  return Object.freeze({
@@ -10,6 +10,10 @@ import type {
10
10
  ToolCall,
11
11
  UserMessage,
12
12
  } from "../agent/types";
13
+ import {
14
+ toolResultText,
15
+ validateToolResultContent,
16
+ } from "../agent/tool-result-content";
13
17
  import type { RuntimeSessionContext } from "../agent/runtime-session";
14
18
  import {
15
19
  parseImageAssetId,
@@ -61,11 +65,31 @@ export function toOpenAIResponsesItems(
61
65
  }
62
66
 
63
67
  if (message.role === "tool") {
68
+ validateToolResultContent(message.content);
69
+ const hasImage = message.content.some((block) => block.type === "image");
64
70
  return [
65
71
  {
66
72
  type: "function_call_output",
67
73
  call_id: message.providerToolCallId,
68
- output: message.content,
74
+ output: hasImage
75
+ ? message.content.map((block) =>
76
+ block.type === "text"
77
+ ? ({ type: "input_text", text: block.text } as const)
78
+ : ({
79
+ type: "input_image",
80
+ detail: "auto" as const,
81
+ image_url:
82
+ options.materializedImages === undefined
83
+ ? (imageAssetUrlMarker(
84
+ block.asset.assetId,
85
+ ) as unknown as string)
86
+ : requireMaterializedImage(
87
+ options.materializedImages,
88
+ block.asset.assetId,
89
+ ),
90
+ } as const),
91
+ )
92
+ : toolResultText(message.content),
69
93
  },
70
94
  ];
71
95
  }
@@ -9,7 +9,7 @@ import {
9
9
  IMAGE_INPUT_POLICY_VERSION,
10
10
  } from "../image/image-input-policy";
11
11
  import type { ModelContextBudget } from "./model-context-profile";
12
- import { ProviderResponseError } from "./model-client";
12
+ import { ProviderResponseError, validateModelModalities } from "./model-client";
13
13
  import type {
14
14
  MaterializedModelRequest,
15
15
  ModelClient,
@@ -23,6 +23,7 @@ import type {
23
23
  } from "./model-client";
24
24
  import {
25
25
  deepFreeze,
26
+ imageToolSegment,
26
27
  imageUserSegment,
27
28
  materializeOpenAIRequest,
28
29
  normalizedEndpointPolicy,
@@ -39,7 +40,7 @@ import { OpenAIResponsesStreamAccumulator } from "./openai-responses-stream";
39
40
  import type { ReasoningEffortController } from "./reasoning-effort";
40
41
  import { sha256, stableJsonStringify } from "./model-request-preflight";
41
42
 
42
- const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-v1";
43
+ const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-v2";
43
44
  const OPENAI_RESPONSES_TIMEOUT_MS = 30 * 60 * 1_000;
44
45
 
45
46
  export class OpenAIResponsesModelClient implements ModelClient {
@@ -48,6 +49,7 @@ export class OpenAIResponsesModelClient implements ModelClient {
48
49
  serializationVersion: OPENAI_RESPONSES_SERIALIZATION_VERSION,
49
50
  });
50
51
  readonly inputModalities: readonly ("text" | "image")[];
52
+ readonly toolResultModalities: readonly ("text" | "image")[];
51
53
  readonly reasoningEffort?: ReasoningEffortController;
52
54
  private readonly client: OpenAI;
53
55
  private readonly preparedRequests = new WeakSet<object>();
@@ -61,6 +63,8 @@ export class OpenAIResponsesModelClient implements ModelClient {
61
63
  contextBudget: ModelContextBudget;
62
64
  baseURL?: string;
63
65
  inputModalities?: readonly ("text" | "image")[];
66
+ toolResultModalities?: readonly ("text" | "image")[];
67
+ profileName?: string;
64
68
  model: string;
65
69
  providerName?: string;
66
70
  reasoningEffort?: ReasoningEffortController;
@@ -72,10 +76,15 @@ export class OpenAIResponsesModelClient implements ModelClient {
72
76
  this.provider = options.providerName ?? "responses-compatible";
73
77
  this.stream = options.stream ?? true;
74
78
  this.reasoningEffort = options.reasoningEffort;
75
- this.inputModalities = Object.freeze([...(options.inputModalities ?? ["text"])]);
76
- if (!this.inputModalities.includes("text")) {
77
- throw new Error('OpenAI Responses input modalities must include "text".');
78
- }
79
+ const modalities = validateModelModalities({
80
+ profileName: options.profileName,
81
+ adapter: this.messageProtocol.adapter,
82
+ inputModalities: options.inputModalities ?? ["text"],
83
+ toolResultModalities: options.toolResultModalities ?? ["text"],
84
+ adapterToolResultModalities: ["text", "image"],
85
+ });
86
+ this.inputModalities = modalities.inputModalities;
87
+ this.toolResultModalities = modalities.toolResultModalities;
79
88
  this.client = new OpenAI({
80
89
  apiKey: options.apiKey,
81
90
  baseURL: options.baseURL,
@@ -113,11 +122,17 @@ export class OpenAIResponsesModelClient implements ModelClient {
113
122
  const messageSegments = input.messages.map(
114
123
  (message, index): PreparedPromptSegment =>
115
124
  message.role === "user" && message.attachments !== undefined
116
- ? imageUserSegment(message)
117
- : {
118
- kind: segmentKind(message.role),
119
- normalizedText: stableJsonStringify(itemsByMessage[index]),
120
- },
125
+ ? imageUserSegment(message, index + 1)
126
+ : message.role === "tool"
127
+ ? imageToolSegment(
128
+ message,
129
+ index + 1,
130
+ stableJsonStringify(itemsByMessage[index]),
131
+ )
132
+ : {
133
+ kind: segmentKind(message.role),
134
+ normalizedText: stableJsonStringify(itemsByMessage[index]),
135
+ },
121
136
  );
122
137
  const mediaOccurrenceCount = messageSegments.reduce(
123
138
  (total, segment) => total + (segment.media?.length ?? 0),
@@ -132,6 +147,7 @@ export class OpenAIResponsesModelClient implements ModelClient {
132
147
  requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
133
148
  stream: this.stream,
134
149
  inputModalities: this.inputModalities,
150
+ toolResultModalities: this.toolResultModalities,
135
151
  requestPolicy: { store: false, toolChoice: "auto" },
136
152
  imagePolicy: {
137
153
  version: IMAGE_INPUT_POLICY_VERSION,
@@ -85,6 +85,16 @@ export function estimatePromptSegments(
85
85
  };
86
86
  }
87
87
 
88
+ export function guardedContextTokens(
89
+ breakdown: RawContextBreakdown,
90
+ correctionFactor: number,
91
+ ): number {
92
+ return (
93
+ Math.ceil(breakdown.textAndProtocolTokens * correctionFactor) +
94
+ breakdown.imageTokens
95
+ );
96
+ }
97
+
88
98
  export class RollingTokenCalibration {
89
99
  private readonly samples: number[] = [];
90
100
 
@@ -1,4 +1,9 @@
1
1
  import type { ToolCall } from "../agent/types";
2
+ import type { ToolResultContent } from "../agent/types";
3
+ import {
4
+ textToolResultContent,
5
+ toolResultDisplayText,
6
+ } from "../agent/tool-result-content";
2
7
  import type {
3
8
  BashRawResult,
4
9
  DeleteFileRawResult,
@@ -6,6 +11,7 @@ import type {
6
11
  GenericToolRawResult,
7
12
  GlobRawResult,
8
13
  GrepRawResult,
14
+ MemoryGetRawResult,
9
15
  MemorySearchRawResult,
10
16
  McpToolRawResult,
11
17
  ReadFileRawResult,
@@ -17,66 +23,100 @@ import type {
17
23
  TaskStopRawResult,
18
24
  ToolRawResult,
19
25
  UpdatePlanRawResult,
26
+ ViewImageRawResult,
27
+ WaitRawResult,
20
28
  WebFetchRawResult,
21
29
  WebSearchRawResult,
22
30
  WriteFileRawResult,
23
31
  } from "../tools/types";
24
32
 
25
33
  export type ToolObservation = {
26
- content: string;
34
+ readonly content: readonly ToolResultContent[];
35
+ readonly displayText: string;
27
36
  };
28
37
 
29
38
  export class ObservationBuilder {
30
39
  build(input: { call: ToolCall; raw: ToolRawResult }): ToolObservation {
31
40
  switch (input.raw.kind) {
32
41
  case "glob":
33
- return { content: renderGlobObservation(input.raw) };
42
+ return textObservation(renderGlobObservation(input.raw));
34
43
  case "grep":
35
- return { content: renderGrepObservation(input.raw) };
44
+ return textObservation(renderGrepObservation(input.raw));
36
45
  case "read":
37
- return { content: renderReadObservation(input.raw) };
46
+ return textObservation(renderReadObservation(input.raw));
47
+ case "view_image":
48
+ return renderViewImageObservation(input.raw);
38
49
  case "recall":
39
- return { content: renderRecallObservation(input.raw) };
50
+ return textObservation(renderRecallObservation(input.raw));
40
51
  case "memory_search":
41
- return { content: renderMemorySearchObservation(input.raw) };
52
+ return textObservation(renderMemorySearchObservation(input.raw));
53
+ case "memory_get":
54
+ return textObservation(renderMemoryGetObservation(input.raw));
42
55
  case "skill":
43
- return { content: renderSkillObservation(input.raw) };
56
+ return textObservation(renderSkillObservation(input.raw));
44
57
  case "write":
45
- return { content: renderWriteObservation(input.raw) };
58
+ return textObservation(renderWriteObservation(input.raw));
46
59
  case "edit":
47
- return { content: renderEditObservation(input.raw) };
60
+ return textObservation(renderEditObservation(input.raw));
48
61
  case "delete":
49
- return { content: renderDeleteObservation(input.raw) };
62
+ return textObservation(renderDeleteObservation(input.raw));
50
63
  case "bash":
51
- return { content: renderBashObservation(input.raw) };
64
+ return textObservation(renderBashObservation(input.raw));
52
65
  case "update_plan":
53
- return { content: renderUpdatePlanObservation(input.raw) };
66
+ return textObservation(renderUpdatePlanObservation(input.raw));
67
+ case "wait":
68
+ return textObservation(renderWaitObservation(input.raw));
54
69
  case "task_list":
55
- return { content: renderTaskListObservation(input.raw) };
70
+ return textObservation(renderTaskListObservation(input.raw));
56
71
  case "task_output":
57
- return { content: renderTaskOutputObservation(input.raw) };
72
+ return textObservation(renderTaskOutputObservation(input.raw));
58
73
  case "task_input":
59
- return { content: renderTaskInputObservation(input.raw) };
74
+ return textObservation(renderTaskInputObservation(input.raw));
60
75
  case "task_stop":
61
- return { content: renderTaskStopObservation(input.raw) };
76
+ return textObservation(renderTaskStopObservation(input.raw));
62
77
  case "web_search":
63
- return { content: renderWebSearchObservation(input.raw) };
78
+ return textObservation(renderWebSearchObservation(input.raw));
64
79
  case "web_fetch":
65
- return { content: renderWebFetchObservation(input.raw) };
80
+ return textObservation(renderWebFetchObservation(input.raw));
66
81
  case "mcp":
67
- return { content: renderMcpObservation(input.raw) };
82
+ return textObservation(renderMcpObservation(input.raw));
68
83
  case "generic":
69
- return { content: renderGenericObservation(input.raw) };
84
+ return textObservation(renderGenericObservation(input.raw));
70
85
  default:
71
86
  return assertNever(input.raw);
72
87
  }
73
88
  }
74
89
  }
75
90
 
91
+ function textObservation(text: string): ToolObservation {
92
+ return Object.freeze({ content: textToolResultContent(text), displayText: text });
93
+ }
94
+
95
+ function renderViewImageObservation(raw: ViewImageRawResult): ToolObservation {
96
+ if (!raw.ok || raw.asset === undefined) {
97
+ return textObservation(
98
+ `ViewImage failed for ${raw.filePath || "(unknown path)"}: ${raw.error ?? "Unknown error."}`,
99
+ );
100
+ }
101
+ const text = `Viewed image ${raw.filePath} (${raw.asset.mimeType}, ${raw.asset.width}x${raw.asset.height}, ${raw.asset.byteLength} bytes, asset=${raw.asset.assetId.slice(0, 12)}…).`;
102
+ const content = Object.freeze([
103
+ Object.freeze({ type: "text" as const, text }),
104
+ Object.freeze({ type: "image" as const, asset: raw.asset }),
105
+ ]);
106
+ const displayText = toolResultDisplayText(content);
107
+ return Object.freeze({ content, displayText });
108
+ }
109
+
76
110
  function renderUpdatePlanObservation(raw: UpdatePlanRawResult): string {
77
111
  return raw.ok ? "Plan updated." : `UpdatePlan failed: ${raw.error}`;
78
112
  }
79
113
 
114
+ function renderWaitObservation(raw: WaitRawResult): string {
115
+ return raw.ok
116
+ ? `Waited ${raw.seconds} second${raw.seconds === 1 ? "" : "s"}.`
117
+ : `Wait failed: ${raw.error}`;
118
+ }
119
+
80
120
  function assertNever(value: never): never {
81
121
  throw new Error(`Unhandled tool raw result: ${JSON.stringify(value)}`);
82
122
  }
@@ -247,16 +287,51 @@ function renderMemorySearchObservation(raw: MemorySearchRawResult): string {
247
287
  if (!raw.ok) {
248
288
  return `MemorySearch unavailable: ${raw.error}`;
249
289
  }
290
+ const degradedNote =
291
+ raw.degraded === "vector"
292
+ ? " vector search unavailable; keyword results only."
293
+ : raw.degraded === "fts"
294
+ ? " keyword search unavailable; vector results only."
295
+ : "";
250
296
  if (raw.matches.length === 0) {
251
- return "MemorySearch found no stored memories.";
297
+ return `MemorySearch found no stored memories.${degradedNote}`;
252
298
  }
253
- const header = `MemorySearch returned ${raw.matches.length} derived memories. They may be stale or wrong; verify current workspace facts.`;
299
+ const header = `MemorySearch returned ${raw.matches.length} derived historical memory records.${degradedNote} They describe past turns and may be stale or wrong; verify current workspace facts with current tools before relying on them.`;
300
+ const footer =
301
+ "Use MemoryGet on a result's memory id when its summary is truncated or you need the exact stored record; use RecallSearch on its source session for the full original context.";
254
302
  return [
255
303
  header,
256
- ...raw.matches.map(
257
- (match, index) =>
258
- `${index + 1}. score=${match.score.toFixed(3)} created_at=${match.createdAt} workspace=${match.sourceWorkspace}\n ${match.text}`,
304
+ ...raw.matches.map((match, index) =>
305
+ [
306
+ `${index + 1}. score=${match.score.toFixed(3)} via=${match.via.join(",")} created_at=${match.createdAt} workspace=${match.sourceWorkspace} session=${match.sourceSessionId} memory=${match.memoryId}`,
307
+ ` ${match.text}`,
308
+ ...(match.summary === "" ? [] : [` summary: ${match.summary}`]),
309
+ ].join("\n"),
259
310
  ),
311
+ footer,
312
+ ].join("\n\n");
313
+ }
314
+
315
+ function renderMemoryGetObservation(raw: MemoryGetRawResult): string {
316
+ if (!raw.ok) {
317
+ return `MemoryGet unavailable: ${raw.error}`;
318
+ }
319
+ if (raw.memory === null) {
320
+ return "MemoryGet found no stored memory with that id.";
321
+ }
322
+ const memory = raw.memory;
323
+ const header =
324
+ "MemoryGet returned one derived historical memory record. It describes a past turn and may be stale or wrong; verify current workspace facts with current tools before relying on it.";
325
+ const footer =
326
+ "Use RecallSearch on the source session when you need the full original context.";
327
+ return [
328
+ header,
329
+ [
330
+ `memory=${memory.memoryId} created_at=${memory.createdAt} workspace=${memory.sourceWorkspace} session=${memory.sourceSessionId} turn=${memory.sourceTurnId}`,
331
+ `text: ${memory.text}`,
332
+ ...(memory.summary === "" ? [] : [`summary: ${memory.summary}`]),
333
+ ].join("\n"),
334
+ footer,
260
335
  ].join("\n\n");
261
336
  }
262
337