@downcity/agent 1.1.331 → 1.1.335

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 (30) hide show
  1. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -1
  2. package/bin/executor/core-engine/CoreEngineContextCompaction.js +62 -6
  3. package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -1
  4. package/bin/executor/messages/SessionAttachmentMapper.d.ts +1 -1
  5. package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
  6. package/bin/executor/messages/SessionAttachmentMapper.js +2 -6
  7. package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
  8. package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
  9. package/bin/executor/messages/SessionMessageCodec.js +63 -1
  10. package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
  11. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  12. package/bin/session/messages/SessionAssistantMessageWriter.js +10 -2
  13. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  14. package/bin/session/messages/SessionMessageCodec.d.ts.map +1 -1
  15. package/bin/session/messages/SessionMessageCodec.js +5 -3
  16. package/bin/session/messages/SessionMessageCodec.js.map +1 -1
  17. package/bin/workspace/tool/FileTools.js +1 -1
  18. package/bin/workspace/tool/FileTools.js.map +1 -1
  19. package/package.json +6 -6
  20. package/scripts/core-engine-context-compaction.test.mjs +15 -2
  21. package/scripts/image-plugin-job.test.mjs +128 -2
  22. package/scripts/session-attachments.test.mjs +2 -3
  23. package/scripts/session-messages.test.mjs +98 -0
  24. package/src/executor/core-engine/CoreEngineContextCompaction.ts +84 -15
  25. package/src/executor/messages/SessionAttachmentMapper.ts +2 -6
  26. package/src/executor/messages/SessionMessageCodec.ts +69 -1
  27. package/src/session/messages/SessionAssistantMessageWriter.ts +14 -2
  28. package/src/session/messages/SessionMessageCodec.ts +7 -3
  29. package/src/workspace/tool/FileTools.ts +1 -1
  30. package/tsconfig.tsbuildinfo +1 -1
@@ -96,10 +96,78 @@ export async function to_model_messages(
96
96
  });
97
97
 
98
98
  // 调用 ai-sdk 的转换函数。
99
- return await convertToModelMessages(input, {
99
+ const converted_messages = await convertToModelMessages(input, {
100
100
  // 如果当前轮有工具,就把工具注入转换选项。
101
101
  ...(tools && Object.keys(tools).length > 0 ? { tools: tools as ToolSet } : {}),
102
102
  // 忽略历史里的不完整工具调用,提升容错性。
103
103
  ignoreIncompleteToolCalls: true,
104
104
  });
105
+ return repair_orphaned_openai_text_references(converted_messages);
106
+ }
107
+
108
+ /**
109
+ * 修复旧版 Session 中“只保存 msg_*、未保存必需 rs_*”的孤立 Responses API 引用。
110
+ *
111
+ * 关键点(中文)
112
+ * - reasoning itemId / encrypted content 与 message itemId 存在时,保留 Provider 原子重放。
113
+ * - 只有 message itemId 时,删除该引用并发送已持久化的普通文本,避免 400。
114
+ * - 不修改 Session canonical source,该修复是可重建的 Provider 投影。
115
+ */
116
+ function repair_orphaned_openai_text_references(
117
+ messages: ModelMessage[],
118
+ ): ModelMessage[] {
119
+ return messages.map((message) => {
120
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
121
+ return message;
122
+ }
123
+ const has_reasoning_replay_data = message.content.some((part) => {
124
+ if (part.type !== "reasoning") return false;
125
+ const openai_options = read_openai_provider_options(part.providerOptions);
126
+ return Boolean(
127
+ (typeof openai_options?.itemId === "string" && openai_options.itemId) ||
128
+ (typeof openai_options?.reasoningEncryptedContent === "string" &&
129
+ openai_options.reasoningEncryptedContent),
130
+ );
131
+ });
132
+ if (has_reasoning_replay_data) return message;
133
+
134
+ let changed = false;
135
+ const content = message.content.map((part) => {
136
+ if (part.type !== "text") return part;
137
+ const provider_options = read_json_record(part.providerOptions);
138
+ const openai_options = read_openai_provider_options(part.providerOptions);
139
+ if (!provider_options || !openai_options || !("itemId" in openai_options)) {
140
+ return part;
141
+ }
142
+ changed = true;
143
+ const { itemId: _item_id, ...remaining_openai_options } = openai_options;
144
+ const next_provider_options = { ...provider_options };
145
+ if (Object.keys(remaining_openai_options).length > 0) {
146
+ next_provider_options.openai = remaining_openai_options;
147
+ } else {
148
+ delete next_provider_options.openai;
149
+ }
150
+ return {
151
+ ...part,
152
+ providerOptions: Object.keys(next_provider_options).length > 0
153
+ ? next_provider_options
154
+ : undefined,
155
+ };
156
+ });
157
+ return changed ? { ...message, content } as ModelMessage : message;
158
+ });
159
+ }
160
+
161
+ /** 读取 Provider options 中的 OpenAI 协议字段。 */
162
+ function read_openai_provider_options(
163
+ value: unknown,
164
+ ): Record<string, unknown> | undefined {
165
+ return read_json_record(read_json_record(value)?.openai);
166
+ }
167
+
168
+ /** 安全读取普通 JSON object。 */
169
+ function read_json_record(value: unknown): Record<string, unknown> | undefined {
170
+ return value && typeof value === "object" && !Array.isArray(value)
171
+ ? value as Record<string, unknown>
172
+ : undefined;
105
173
  }
@@ -156,9 +156,21 @@ export class SessionAssistantMessageWriter {
156
156
  const source_part_id = this.source_text_part_id(type, chunk.id);
157
157
  const part_id = this.active_text_part_ids.get(source_part_id);
158
158
  if (!part_id) return;
159
- const part = current.parts.find((item) => item.part_id === part_id);
159
+ const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
160
+ const pending = this.pending_text_parts.get(part_id);
161
+ // 关键点(中文):Responses API 可能只输出 reasoning start/end 与 itemId,
162
+ // 却没有可见 reasoning delta。这个空 Part 不是 UI 占位,而是后续 msg_* 重放必需的协议关联。
163
+ if (
164
+ type === "reasoning" &&
165
+ !current.parts.some((item) => item.part_id === part_id) &&
166
+ (provider_metadata !== undefined || pending?.provider_metadata !== undefined)
167
+ ) {
168
+ await this.ensure_text_part(part_id, type, provider_metadata);
169
+ }
170
+ const part = this.current_message().parts.find(
171
+ (item) => item.part_id === part_id,
172
+ );
160
173
  if (part?.type === "text" || part?.type === "reasoning") {
161
- const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
162
174
  await this.upsert_part({
163
175
  ...part,
164
176
  state: "done",
@@ -154,10 +154,14 @@ export function from_ui_assistant_parts(
154
154
  const type = String(candidate.type || "");
155
155
  if (type === "text" || type === "reasoning") {
156
156
  const text = String(candidate.text || "");
157
- // 关键点(中文):AI SDK 会为只有 start/end、没有 delta 的流生成空占位 Part。
158
- // canonical history 不保存无内容的协议占位,避免与只按 delta 创建 Part 的 writer 分叉。
159
- if (text.length === 0) return [];
160
157
  const provider_metadata = to_session_provider_metadata(candidate.providerMetadata);
158
+ // 关键点(中文):AI SDK 会为只有 start/end、没有 delta 的流生成空占位 Part。
159
+ // 纯空占位不保存;但 Responses API 的 reasoning 即使没有可见文本,
160
+ // 也可能通过 itemId / encrypted content 与后续 message 形成必须原子重放的协议组。
161
+ if (
162
+ text.length === 0 &&
163
+ !(type === "reasoning" && provider_metadata !== undefined)
164
+ ) return [];
161
165
  return [{
162
166
  part_id: `${type}:${index + 1}`,
163
167
  sequence: index + 1,
@@ -29,7 +29,7 @@ import {
29
29
  export function create_file_tools(runner: FileToolRunner): FileToolSet {
30
30
  const read = tool({
31
31
  description:
32
- "Read a project file instead of using cat or sed. Images and PDFs are attached to the next model step as local file parts. Text output is limited to 500 lines and 256KB by default; use offset and limit to continue. Other binary files return metadata only.",
32
+ "Read a project file instead of using cat or sed. For a chat attachment, use the file part's url (the .downcity/... storage path) as path; filename is only the user's display name and is not a project path. Images and PDFs are attached to the next model step as local file parts. Text output is limited to 500 lines and 256KB by default; use offset and limit to continue. Other binary files return metadata only.",
33
33
  inputSchema: read_file_tool_input_schema,
34
34
  execute: async (
35
35
  input: ReadFileToolInput,