@teammy/server 0.1.12 → 0.1.14

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 (26) hide show
  1. package/README.md +1 -1
  2. package/dist/main.js +3190 -2575
  3. package/dist/native-pi-entry.js +211 -6
  4. package/dist/project-pi-entry.js +219 -11
  5. package/dist/runtime/README.md +3 -3
  6. package/dist/runtime/shared/tools.json +1 -1
  7. package/dist/runtime/workflows/session-handoff/config.json +2 -2
  8. package/dist/runtime/workflows/task-bootstrap/config.json +1 -1
  9. package/dist/runtime/workflows/task-bootstrap/prompt.md +4 -2
  10. package/dist/web/assets/{Assistant-o-N3hQtn.js → Assistant-Bdd5Nsup.js} +1 -1
  11. package/dist/web/assets/Dashboard-Bkdiv8g6.js +1 -0
  12. package/dist/web/assets/{Files-BvHbF1eW.js → Files-DrkZfsY3.js} +1 -1
  13. package/dist/web/assets/{HtmlPreview-Uzm5MRkj.js → HtmlPreview-BrlipR_1.js} +1 -1
  14. package/dist/web/assets/{Markdown-SFOHcUZw.js → Markdown-VmpJdXUC.js} +1 -1
  15. package/dist/web/assets/{Materials-DjvULSPf.js → Materials-CirZh-et.js} +1 -1
  16. package/dist/web/assets/Sessions-C88uXxYK.js +1 -0
  17. package/dist/web/assets/{WorkspaceTree-B7mZepdd.js → WorkspaceTree-DiWj8PhE.js} +1 -1
  18. package/dist/web/assets/{file-links-BHeuStKI.js → file-links-Dpr2sN4y.js} +1 -1
  19. package/dist/web/assets/{index-BW6ule39.js → index-BTcnf4jy.js} +3 -3
  20. package/dist/web/assets/project-links-Dy6-B89_.js +1 -0
  21. package/dist/web/downloads/teammy-skill.zip +0 -0
  22. package/dist/web/index.html +1 -1
  23. package/package.json +1 -1
  24. package/dist/web/assets/Dashboard-BkoNYNTq.js +0 -1
  25. package/dist/web/assets/Sessions-BbxEbr9w.js +0 -1
  26. package/dist/web/assets/project-links-XbF78Bal.js +0 -1
@@ -1,5 +1,5 @@
1
1
  // apps/server/ai/native-pi-entry.ts
2
- import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type2 } from "@earendil-works/pi-ai";
2
+ import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type3 } from "@earendil-works/pi-ai";
3
3
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
4
4
  import {
5
5
  createAgentSession,
@@ -8,7 +8,7 @@ import {
8
8
  SessionManager,
9
9
  SettingsManager
10
10
  } from "@earendil-works/pi-coding-agent";
11
- import { z as z2 } from "zod";
11
+ import { z as z4 } from "zod";
12
12
 
13
13
  // apps/server/ai/agent-channel.ts
14
14
  import { createInterface } from "node:readline";
@@ -60,6 +60,20 @@ import { createModels, createProvider, Type } from "@earendil-works/pi-ai";
60
60
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
61
61
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
62
62
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
63
+
64
+ // apps/server/errors.ts
65
+ var HubError = class extends Error {
66
+ constructor(statusCode, code, message) {
67
+ super(message);
68
+ this.statusCode = statusCode;
69
+ this.code = code;
70
+ this.name = "HubError";
71
+ }
72
+ statusCode;
73
+ code;
74
+ };
75
+
76
+ // apps/server/ai/models.ts
63
77
  function modelAccess(profile, key, reasoning = false) {
64
78
  const model = {
65
79
  id: profile.model,
@@ -109,6 +123,196 @@ function loadAgentSkills(directory = `${agentResourcesPath}/skills`) {
109
123
  return existsSync(directory) ? loadSkillsFromDir({ dir: directory, source: "teammy-deployment" }) : { skills: [], diagnostics: [] };
110
124
  }
111
125
 
126
+ // apps/server/ai/context-content.ts
127
+ import { createHash, randomUUID } from "node:crypto";
128
+ import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
129
+ import { join } from "node:path";
130
+ import { z as z2 } from "zod";
131
+ var CONTENT_BYTES = 32 * 1024;
132
+ var CONTENT_LINES = 500;
133
+ var json = (value) => JSON.stringify(value) ?? "null";
134
+ function contentFits(value, bytes = CONTENT_BYTES) {
135
+ const text = json(value);
136
+ return Buffer.byteLength(text) <= bytes && (text.match(/\\n|\n/g)?.length ?? 0) < CONTENT_LINES;
137
+ }
138
+ var ReadToolResultSchema = z2.object({
139
+ id: z2.string().regex(/^[a-f0-9]{64}$/),
140
+ path: z2.array(z2.union([z2.string().max(512), z2.number().int().nonnegative()])).max(32).refine((path) => Buffer.byteLength(JSON.stringify(path)) <= 4096, "path \u8FC7\u957F").default([]),
141
+ offset: z2.number().int().nonnegative().default(0),
142
+ limit: z2.number().int().positive().max(16e3).optional()
143
+ }).strict();
144
+ var kind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
145
+ var length = (value) => typeof value === "string" || Array.isArray(value) ? value.length : value && typeof value === "object" ? Object.keys(value).length : 1;
146
+ function textPrefix(text, end) {
147
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1] ?? "")) end--;
148
+ return text.slice(0, end);
149
+ }
150
+ function overview(value) {
151
+ if (contentFits(value, 1024)) return value;
152
+ if (typeof value === "string")
153
+ return { kind: "string", length: value.length, preview: textPrefix(value, 300) };
154
+ if (Array.isArray(value)) return { kind: "array", length: value.length };
155
+ if (value && typeof value === "object")
156
+ return {
157
+ kind: "object",
158
+ fields: Object.entries(value).slice(0, 12).map(([key, item]) => ({
159
+ key: textPrefix(key, 80),
160
+ kind: kind(item),
161
+ length: length(item),
162
+ ...typeof item === "string" ? { preview: textPrefix(item, 120) } : {}
163
+ })),
164
+ totalFields: Object.keys(value).length
165
+ };
166
+ return value;
167
+ }
168
+ var ContextContent = class {
169
+ constructor(directory) {
170
+ this.directory = directory;
171
+ }
172
+ directory;
173
+ async present(value, source) {
174
+ if (contentFits(value)) return value;
175
+ const body = json({ source, value }), id = createHash("sha256").update(body).digest("hex");
176
+ await mkdir(this.directory, { recursive: true, mode: 448 });
177
+ const temporary = join(this.directory, randomUUID() + ".tmp");
178
+ try {
179
+ await writeFile(temporary, body, { flag: "wx", mode: 384 });
180
+ try {
181
+ await link(temporary, join(this.directory, id + ".json"));
182
+ } catch (error) {
183
+ if (error.code !== "EEXIST") throw error;
184
+ }
185
+ } finally {
186
+ await rm(temporary, { force: true });
187
+ }
188
+ return {
189
+ folded: true,
190
+ note: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u6298\u53E0\uFF1B\u4E0B\u65B9\u4EC5\u4E3A\u6982\u89C8\uFF0C\u4E0D\u80FD\u89C6\u4E3A\u5DF2\u8BFB\u5168\u6587\u3002\u7528 read_tool_result \u6309 path \u548C offset \u8BFB\u53D6\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u5FC5\u91CD\u590D\u6267\u884C\u539F\u5DE5\u5177\u3002",
191
+ source,
192
+ id,
193
+ bytes: Buffer.byteLength(json(value)),
194
+ overview: overview(value),
195
+ read: { tool: "read_tool_result", arguments: { id } }
196
+ };
197
+ }
198
+ async input(text) {
199
+ const value = await this.present(text, "\u672C\u8F6E\u8F93\u5165");
200
+ return typeof value === "string" ? value : "\u672C\u8F6E\u8F93\u5165\u8F83\u957F\uFF0C\u539F\u6587\u5DF2\u5B8C\u6574\u4FDD\u5B58\u3002\u5148\u5206\u6BB5\u8BFB\u53D6\uFF0C\u786E\u8BA4\u7528\u6237\u7684\u76EE\u6807\u3001\u6761\u4EF6\u548C\u9650\u5236\u540E\u518D\u884C\u52A8\uFF1B\u6982\u89C8\u4E0D\u662F\u5168\u90E8\u8F93\u5165\u3002\n" + json(value);
201
+ }
202
+ async read(input) {
203
+ const { id, path, offset, limit } = ReadToolResultSchema.parse(input);
204
+ let body;
205
+ try {
206
+ body = await readFile(join(this.directory, id + ".json"), "utf8");
207
+ } catch {
208
+ throw new HubError(
209
+ 404,
210
+ "TOOL_RESULT_NOT_FOUND",
211
+ "\u6B64\u7ED3\u679C\u4E0D\u5728\u5F53\u524D\u4F1A\u8BDD\u6216\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002"
212
+ );
213
+ }
214
+ if (createHash("sha256").update(body).digest("hex") !== id)
215
+ throw new HubError(409, "TOOL_RESULT_CHANGED", "\u7ED3\u679C\u5FEB\u7167\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002");
216
+ const saved = JSON.parse(body);
217
+ let value = saved.value;
218
+ for (const key of path) {
219
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
220
+ throw new HubError(400, "TOOL_RESULT_PATH", "\u7ED3\u679C\u4E2D\u4E0D\u5B58\u5728\u8BE5 path\u3002");
221
+ value = value[key];
222
+ }
223
+ const total = length(value);
224
+ if (offset > total) throw new HubError(400, "TOOL_RESULT_OFFSET", "offset \u8D85\u51FA\u7ED3\u679C\u8303\u56F4\u3002");
225
+ const base = { id, source: saved.source, path, kind: kind(value), total, offset };
226
+ if (typeof value === "string") {
227
+ if (/[\uDC00-\uDFFF]/.test(value[offset] ?? "") && /[\uD800-\uDBFF]/.test(value[offset - 1] ?? ""))
228
+ throw new HubError(400, "TOOL_RESULT_OFFSET", "\u8BF7\u6CBF\u7528\u4E0A\u6B21\u8FD4\u56DE\u7684 nextOffset\u3002");
229
+ const tail = value.slice(offset);
230
+ let lo = 0, hi = Math.min(tail.length, limit ?? 16e3);
231
+ while (lo < hi) {
232
+ const mid = Math.ceil((lo + hi) / 2);
233
+ if (contentFits({ ...base, text: textPrefix(tail, mid), nextOffset: offset + mid }))
234
+ lo = mid;
235
+ else hi = mid - 1;
236
+ }
237
+ let text = textPrefix(tail, lo);
238
+ if (!text && tail) text = textPrefix(tail, 2);
239
+ return {
240
+ ...base,
241
+ text,
242
+ nextOffset: offset + text.length < total ? offset + text.length : null
243
+ };
244
+ }
245
+ if (!value || typeof value !== "object") return { ...base, value, nextOffset: null };
246
+ const entries = [];
247
+ const keys = Array.isArray(value) ? value.map((_, i) => i) : Object.keys(value);
248
+ for (const key of keys.slice(offset, offset + Math.min(limit ?? 50, 100))) {
249
+ const item = value[key];
250
+ const folded = !contentFits(item, 2048);
251
+ const entry = { path: [...path, key], folded, value: folded ? overview(item) : item };
252
+ if (!contentFits({
253
+ ...base,
254
+ entries: [...entries, entry],
255
+ nextOffset: offset + entries.length + 1
256
+ }))
257
+ break;
258
+ entries.push(entry);
259
+ }
260
+ if (!entries.length && offset < total)
261
+ throw new HubError(413, "TOOL_RESULT_KEY_TOO_LONG", "\u7ED3\u679C\u5B57\u6BB5\u540D\u8FC7\u957F\uFF0C\u65E0\u6CD5\u751F\u6210\u5B89\u5168\u7684\u5206\u9875\u3002");
262
+ return {
263
+ ...base,
264
+ entries,
265
+ nextOffset: offset + entries.length < total ? offset + entries.length : null
266
+ };
267
+ }
268
+ };
269
+
270
+ // apps/server/ai/content-tools.ts
271
+ import { Type as Type2 } from "@earendil-works/pi-ai";
272
+ import { z as z3 } from "zod";
273
+ var readToolResultDescription = "\u8BFB\u53D6\u5F53\u524D\u8303\u56F4\u5185\u5DF2\u6298\u53E0\u7684\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u91CD\u8DD1\u539F\u5DE5\u5177\u3002\u5148\u7528 id \u67E5\u770B\u5B57\u6BB5/\u6761\u76EE\uFF1B\u6CBF\u8FD4\u56DE\u7684 path \u8BFB\u53D6\u5177\u4F53\u5B57\u6BB5\u6216\u6587\u4EF6\u5DEE\u5F02\uFF0CnextOffset \u975E\u7A7A\u65F6\u7EE7\u7EED\u3002folded=true \u7684\u6761\u76EE\u53EA\u663E\u793A\u6982\u89C8\uFF0C\u4E0D\u80FD\u5F53\u4F5C\u5B8C\u6574\u6B63\u6587\uFF1B\u5B57\u7B26\u4E32 offset \u4E3A\u8FD4\u56DE\u7684 UTF-16 \u4F4D\u7F6E\u3002\u5FEB\u7167\u56FA\u5B9A\u5728\u539F\u67E5\u8BE2\u7248\u672C\u3002";
274
+ function contentTools(content, tools) {
275
+ return [
276
+ ...tools.map((tool) => ({
277
+ ...tool,
278
+ async execute(...args) {
279
+ const result = await tool.execute(...args);
280
+ const blocks = [];
281
+ for (const block of result.content) {
282
+ if (block.type !== "text") {
283
+ blocks.push(block);
284
+ continue;
285
+ }
286
+ let value;
287
+ try {
288
+ value = JSON.parse(block.text);
289
+ } catch {
290
+ value = block.text;
291
+ }
292
+ const bounded = await content.present(value, tool.name);
293
+ blocks.push({
294
+ type: "text",
295
+ text: bounded === value && contentFits(block.text) ? block.text : JSON.stringify(bounded)
296
+ });
297
+ }
298
+ return { content: blocks, details: {} };
299
+ }
300
+ })),
301
+ {
302
+ name: "read_tool_result",
303
+ label: "\u8BFB\u53D6\u6298\u53E0\u5185\u5BB9",
304
+ description: readToolResultDescription,
305
+ parameters: Type2.Unsafe(z3.toJSONSchema(ReadToolResultSchema, { io: "input" })),
306
+ async execute(_id, input) {
307
+ return {
308
+ content: [{ type: "text", text: JSON.stringify(await content.read(input)) }],
309
+ details: {}
310
+ };
311
+ }
312
+ }
313
+ ];
314
+ }
315
+
112
316
  // apps/server/ai/native-pi-entry.ts
113
317
  var channel = new AgentChannel(process.stdin, process.stdout);
114
318
  var sequence = 0;
@@ -123,6 +327,7 @@ function rpc(request) {
123
327
  async function run(config) {
124
328
  const { model, runtime } = config;
125
329
  const cwd = "/workspaces", agentDir = "/tmp/pi";
330
+ const content = new ContextContent("/tmp/teammy-content");
126
331
  const modelRuntime = await ModelRuntime.create({
127
332
  credentials: new InMemoryCredentialStore(),
128
333
  modelsStore: new InMemoryModelsStore(),
@@ -163,7 +368,7 @@ async function run(config) {
163
368
  const query = {
164
369
  name: "query_tasks",
165
370
  ...runtime.toolText.query_tasks,
166
- parameters: Type2.Unsafe(z2.toJSONSchema(TaskQuerySchema)),
371
+ parameters: Type3.Unsafe(z4.toJSONSchema(TaskQuerySchema)),
167
372
  async execute(_id, params) {
168
373
  const value = await rpc({ method: "query_tasks", input: TaskQuerySchema.parse(params) });
169
374
  return { content: [{ type: "text", text: JSON.stringify(value) }], details: {} };
@@ -182,8 +387,8 @@ async function run(config) {
182
387
  retry: { enabled: false, maxRetries: 0 },
183
388
  cacheWarming: "off"
184
389
  }),
185
- tools: runtime.config.tools,
186
- customTools: runtime.config.tools.includes("query_tasks") ? [query] : []
390
+ tools: [...runtime.config.tools, "read_tool_result"],
391
+ customTools: contentTools(content, runtime.config.tools.includes("query_tasks") ? [query] : [])
187
392
  });
188
393
  session.agent.streamFunction = (_model, context) => {
189
394
  const stream = createAssistantMessageEventStream();
@@ -227,7 +432,7 @@ async function run(config) {
227
432
  });
228
433
  });
229
434
  try {
230
- await session.prompt(config.input);
435
+ await session.prompt(await content.input(config.input));
231
436
  const last = session.messages.filter((message) => message.role === "assistant").at(-1);
232
437
  if (!last || last.stopReason === "error" || last.stopReason === "aborted")
233
438
  throw new Error("Analysis failed.");
@@ -1,9 +1,9 @@
1
1
  // apps/server/agents/pi-entry.ts
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync as existsSync2 } from "node:fs";
4
- import { mkdir, readFile } from "node:fs/promises";
5
- import { join, basename } from "node:path";
6
- import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type2 } from "@earendil-works/pi-ai";
4
+ import { mkdir as mkdir2, readFile as readFile2 } from "node:fs/promises";
5
+ import { join as join2, basename } from "node:path";
6
+ import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type3 } from "@earendil-works/pi-ai";
7
7
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
8
8
  import {
9
9
  createAgentSession,
@@ -54,6 +54,20 @@ import { createModels, createProvider, Type } from "@earendil-works/pi-ai";
54
54
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
55
55
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
56
56
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
57
+
58
+ // apps/server/errors.ts
59
+ var HubError = class extends Error {
60
+ constructor(statusCode, code, message) {
61
+ super(message);
62
+ this.statusCode = statusCode;
63
+ this.code = code;
64
+ this.name = "HubError";
65
+ }
66
+ statusCode;
67
+ code;
68
+ };
69
+
70
+ // apps/server/ai/models.ts
57
71
  function modelAccess(profile, key, reasoning = false) {
58
72
  const model = {
59
73
  id: profile.model,
@@ -103,6 +117,196 @@ function loadAgentSkills(directory = `${agentResourcesPath}/skills`) {
103
117
  return existsSync(directory) ? loadSkillsFromDir({ dir: directory, source: "teammy-deployment" }) : { skills: [], diagnostics: [] };
104
118
  }
105
119
 
120
+ // apps/server/ai/context-content.ts
121
+ import { createHash, randomUUID } from "node:crypto";
122
+ import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
123
+ import { join } from "node:path";
124
+ import { z } from "zod";
125
+ var CONTENT_BYTES = 32 * 1024;
126
+ var CONTENT_LINES = 500;
127
+ var json = (value) => JSON.stringify(value) ?? "null";
128
+ function contentFits(value, bytes = CONTENT_BYTES) {
129
+ const text = json(value);
130
+ return Buffer.byteLength(text) <= bytes && (text.match(/\\n|\n/g)?.length ?? 0) < CONTENT_LINES;
131
+ }
132
+ var ReadToolResultSchema = z.object({
133
+ id: z.string().regex(/^[a-f0-9]{64}$/),
134
+ path: z.array(z.union([z.string().max(512), z.number().int().nonnegative()])).max(32).refine((path) => Buffer.byteLength(JSON.stringify(path)) <= 4096, "path \u8FC7\u957F").default([]),
135
+ offset: z.number().int().nonnegative().default(0),
136
+ limit: z.number().int().positive().max(16e3).optional()
137
+ }).strict();
138
+ var kind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
139
+ var length = (value) => typeof value === "string" || Array.isArray(value) ? value.length : value && typeof value === "object" ? Object.keys(value).length : 1;
140
+ function textPrefix(text, end) {
141
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1] ?? "")) end--;
142
+ return text.slice(0, end);
143
+ }
144
+ function overview(value) {
145
+ if (contentFits(value, 1024)) return value;
146
+ if (typeof value === "string")
147
+ return { kind: "string", length: value.length, preview: textPrefix(value, 300) };
148
+ if (Array.isArray(value)) return { kind: "array", length: value.length };
149
+ if (value && typeof value === "object")
150
+ return {
151
+ kind: "object",
152
+ fields: Object.entries(value).slice(0, 12).map(([key, item]) => ({
153
+ key: textPrefix(key, 80),
154
+ kind: kind(item),
155
+ length: length(item),
156
+ ...typeof item === "string" ? { preview: textPrefix(item, 120) } : {}
157
+ })),
158
+ totalFields: Object.keys(value).length
159
+ };
160
+ return value;
161
+ }
162
+ var ContextContent = class {
163
+ constructor(directory) {
164
+ this.directory = directory;
165
+ }
166
+ directory;
167
+ async present(value, source) {
168
+ if (contentFits(value)) return value;
169
+ const body = json({ source, value }), id = createHash("sha256").update(body).digest("hex");
170
+ await mkdir(this.directory, { recursive: true, mode: 448 });
171
+ const temporary = join(this.directory, randomUUID() + ".tmp");
172
+ try {
173
+ await writeFile(temporary, body, { flag: "wx", mode: 384 });
174
+ try {
175
+ await link(temporary, join(this.directory, id + ".json"));
176
+ } catch (error) {
177
+ if (error.code !== "EEXIST") throw error;
178
+ }
179
+ } finally {
180
+ await rm(temporary, { force: true });
181
+ }
182
+ return {
183
+ folded: true,
184
+ note: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u6298\u53E0\uFF1B\u4E0B\u65B9\u4EC5\u4E3A\u6982\u89C8\uFF0C\u4E0D\u80FD\u89C6\u4E3A\u5DF2\u8BFB\u5168\u6587\u3002\u7528 read_tool_result \u6309 path \u548C offset \u8BFB\u53D6\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u5FC5\u91CD\u590D\u6267\u884C\u539F\u5DE5\u5177\u3002",
185
+ source,
186
+ id,
187
+ bytes: Buffer.byteLength(json(value)),
188
+ overview: overview(value),
189
+ read: { tool: "read_tool_result", arguments: { id } }
190
+ };
191
+ }
192
+ async input(text) {
193
+ const value = await this.present(text, "\u672C\u8F6E\u8F93\u5165");
194
+ return typeof value === "string" ? value : "\u672C\u8F6E\u8F93\u5165\u8F83\u957F\uFF0C\u539F\u6587\u5DF2\u5B8C\u6574\u4FDD\u5B58\u3002\u5148\u5206\u6BB5\u8BFB\u53D6\uFF0C\u786E\u8BA4\u7528\u6237\u7684\u76EE\u6807\u3001\u6761\u4EF6\u548C\u9650\u5236\u540E\u518D\u884C\u52A8\uFF1B\u6982\u89C8\u4E0D\u662F\u5168\u90E8\u8F93\u5165\u3002\n" + json(value);
195
+ }
196
+ async read(input) {
197
+ const { id, path, offset, limit } = ReadToolResultSchema.parse(input);
198
+ let body;
199
+ try {
200
+ body = await readFile(join(this.directory, id + ".json"), "utf8");
201
+ } catch {
202
+ throw new HubError(
203
+ 404,
204
+ "TOOL_RESULT_NOT_FOUND",
205
+ "\u6B64\u7ED3\u679C\u4E0D\u5728\u5F53\u524D\u4F1A\u8BDD\u6216\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002"
206
+ );
207
+ }
208
+ if (createHash("sha256").update(body).digest("hex") !== id)
209
+ throw new HubError(409, "TOOL_RESULT_CHANGED", "\u7ED3\u679C\u5FEB\u7167\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002");
210
+ const saved = JSON.parse(body);
211
+ let value = saved.value;
212
+ for (const key of path) {
213
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
214
+ throw new HubError(400, "TOOL_RESULT_PATH", "\u7ED3\u679C\u4E2D\u4E0D\u5B58\u5728\u8BE5 path\u3002");
215
+ value = value[key];
216
+ }
217
+ const total = length(value);
218
+ if (offset > total) throw new HubError(400, "TOOL_RESULT_OFFSET", "offset \u8D85\u51FA\u7ED3\u679C\u8303\u56F4\u3002");
219
+ const base = { id, source: saved.source, path, kind: kind(value), total, offset };
220
+ if (typeof value === "string") {
221
+ if (/[\uDC00-\uDFFF]/.test(value[offset] ?? "") && /[\uD800-\uDBFF]/.test(value[offset - 1] ?? ""))
222
+ throw new HubError(400, "TOOL_RESULT_OFFSET", "\u8BF7\u6CBF\u7528\u4E0A\u6B21\u8FD4\u56DE\u7684 nextOffset\u3002");
223
+ const tail = value.slice(offset);
224
+ let lo = 0, hi = Math.min(tail.length, limit ?? 16e3);
225
+ while (lo < hi) {
226
+ const mid = Math.ceil((lo + hi) / 2);
227
+ if (contentFits({ ...base, text: textPrefix(tail, mid), nextOffset: offset + mid }))
228
+ lo = mid;
229
+ else hi = mid - 1;
230
+ }
231
+ let text = textPrefix(tail, lo);
232
+ if (!text && tail) text = textPrefix(tail, 2);
233
+ return {
234
+ ...base,
235
+ text,
236
+ nextOffset: offset + text.length < total ? offset + text.length : null
237
+ };
238
+ }
239
+ if (!value || typeof value !== "object") return { ...base, value, nextOffset: null };
240
+ const entries = [];
241
+ const keys = Array.isArray(value) ? value.map((_, i) => i) : Object.keys(value);
242
+ for (const key of keys.slice(offset, offset + Math.min(limit ?? 50, 100))) {
243
+ const item = value[key];
244
+ const folded = !contentFits(item, 2048);
245
+ const entry = { path: [...path, key], folded, value: folded ? overview(item) : item };
246
+ if (!contentFits({
247
+ ...base,
248
+ entries: [...entries, entry],
249
+ nextOffset: offset + entries.length + 1
250
+ }))
251
+ break;
252
+ entries.push(entry);
253
+ }
254
+ if (!entries.length && offset < total)
255
+ throw new HubError(413, "TOOL_RESULT_KEY_TOO_LONG", "\u7ED3\u679C\u5B57\u6BB5\u540D\u8FC7\u957F\uFF0C\u65E0\u6CD5\u751F\u6210\u5B89\u5168\u7684\u5206\u9875\u3002");
256
+ return {
257
+ ...base,
258
+ entries,
259
+ nextOffset: offset + entries.length < total ? offset + entries.length : null
260
+ };
261
+ }
262
+ };
263
+
264
+ // apps/server/ai/content-tools.ts
265
+ import { Type as Type2 } from "@earendil-works/pi-ai";
266
+ import { z as z2 } from "zod";
267
+ var readToolResultDescription = "\u8BFB\u53D6\u5F53\u524D\u8303\u56F4\u5185\u5DF2\u6298\u53E0\u7684\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u91CD\u8DD1\u539F\u5DE5\u5177\u3002\u5148\u7528 id \u67E5\u770B\u5B57\u6BB5/\u6761\u76EE\uFF1B\u6CBF\u8FD4\u56DE\u7684 path \u8BFB\u53D6\u5177\u4F53\u5B57\u6BB5\u6216\u6587\u4EF6\u5DEE\u5F02\uFF0CnextOffset \u975E\u7A7A\u65F6\u7EE7\u7EED\u3002folded=true \u7684\u6761\u76EE\u53EA\u663E\u793A\u6982\u89C8\uFF0C\u4E0D\u80FD\u5F53\u4F5C\u5B8C\u6574\u6B63\u6587\uFF1B\u5B57\u7B26\u4E32 offset \u4E3A\u8FD4\u56DE\u7684 UTF-16 \u4F4D\u7F6E\u3002\u5FEB\u7167\u56FA\u5B9A\u5728\u539F\u67E5\u8BE2\u7248\u672C\u3002";
268
+ function contentTools(content, tools) {
269
+ return [
270
+ ...tools.map((tool) => ({
271
+ ...tool,
272
+ async execute(...args) {
273
+ const result = await tool.execute(...args);
274
+ const blocks = [];
275
+ for (const block of result.content) {
276
+ if (block.type !== "text") {
277
+ blocks.push(block);
278
+ continue;
279
+ }
280
+ let value;
281
+ try {
282
+ value = JSON.parse(block.text);
283
+ } catch {
284
+ value = block.text;
285
+ }
286
+ const bounded = await content.present(value, tool.name);
287
+ blocks.push({
288
+ type: "text",
289
+ text: bounded === value && contentFits(block.text) ? block.text : JSON.stringify(bounded)
290
+ });
291
+ }
292
+ return { content: blocks, details: {} };
293
+ }
294
+ })),
295
+ {
296
+ name: "read_tool_result",
297
+ label: "\u8BFB\u53D6\u6298\u53E0\u5185\u5BB9",
298
+ description: readToolResultDescription,
299
+ parameters: Type2.Unsafe(z2.toJSONSchema(ReadToolResultSchema, { io: "input" })),
300
+ async execute(_id, input) {
301
+ return {
302
+ content: [{ type: "text", text: JSON.stringify(await content.read(input)) }],
303
+ details: {}
304
+ };
305
+ }
306
+ }
307
+ ];
308
+ }
309
+
106
310
  // apps/server/agents/pi-entry.ts
107
311
  async function piChild() {
108
312
  const channel = new AgentChannel(process.stdin, process.stdout);
@@ -128,7 +332,7 @@ async function piChild() {
128
332
  ]);
129
333
  if (session && key === configKey) return;
130
334
  session?.dispose();
131
- await mkdir(input.sessionDir, { recursive: true });
335
+ await mkdir2(input.sessionDir, { recursive: true });
132
336
  manager = input.transient ? SessionManager.inMemory(input.cwd ?? "/project") : input.sessionFile && existsSync2(input.sessionFile) ? SessionManager.open(input.sessionFile, input.sessionDir, input.cwd ?? "/project") : SessionManager.create(input.cwd ?? "/project", input.sessionDir);
133
337
  const modelRuntime = await ModelRuntime.create({
134
338
  credentials: new InMemoryCredentialStore(),
@@ -170,23 +374,24 @@ async function piChild() {
170
374
  reload: async () => {
171
375
  }
172
376
  };
377
+ const content = new ContextContent(join2(input.sessionDir, "content"));
173
378
  const customTools = input.tools.map((tool) => ({
174
379
  name: tool.name,
175
380
  label: tool.label,
176
381
  description: tool.description,
177
- parameters: Type2.Unsafe(tool.schema),
382
+ parameters: Type3.Unsafe(tool.schema),
178
383
  execute: async (_id, params) => {
179
384
  let input2 = params;
180
385
  if (tool.name === "save_material") {
181
386
  const source = params;
182
387
  input2 = {
183
388
  ...source,
184
- ...source.contentFile ? { _content: await readFile(source.contentFile, "utf8") } : {},
389
+ ...source.contentFile ? { _content: await readFile2(source.contentFile, "utf8") } : {},
185
390
  ...source.originals ? {
186
391
  _files: await Promise.all(
187
392
  source.originals.map(async (file) => ({
188
393
  name: file.name ?? basename(file.path),
189
- base64: (await readFile(file.path)).toString("base64")
394
+ base64: (await readFile2(file.path)).toString("base64")
190
395
  }))
191
396
  )
192
397
  } : {}
@@ -198,7 +403,7 @@ async function piChild() {
198
403
  }));
199
404
  const created = await createAgentSession({
200
405
  cwd: input.cwd ?? "/project",
201
- agentDir: join(input.sessionDir, "agent"),
406
+ agentDir: join2(input.sessionDir, "agent"),
202
407
  modelRuntime,
203
408
  model: input.model,
204
409
  thinkingLevel: "off",
@@ -216,9 +421,10 @@ async function piChild() {
216
421
  "find",
217
422
  "ls",
218
423
  ...input.cwd === "/work" ? ["write", "edit"] : [],
219
- ...input.tools.map((tool) => tool.name)
424
+ ...input.tools.map((tool) => tool.name),
425
+ "read_tool_result"
220
426
  ],
221
- customTools
427
+ customTools: contentTools(content, customTools)
222
428
  });
223
429
  session = created.session;
224
430
  configKey = key;
@@ -298,8 +504,10 @@ async function piChild() {
298
504
  });
299
505
  try {
300
506
  await native.prompt(
301
- interrupted ? `\u6B64\u524D\u8FD9\u4E00\u8F6E\u6267\u884C\u4E2D\u65AD\u6216\u672A\u6210\u529F\uFF0C\u8BF7\u7ED3\u5408\u5DF2\u4FDD\u5B58\u7684\u4E0A\u4E0B\u6587\u7EE7\u7EED\u3002\u5DF2\u63D0\u4EA4\u64CD\u4F5C\u7684\u56DE\u6267\u4ECD\u6709\u6548\uFF0C\u4E0D\u8981\u91CD\u590D\u53D1\u9001\uFF1B\u6682\u5B58\u56DE\u7B54\u548C\u5B8C\u6210\u62A5\u544A\u5C1A\u672A\u63D0\u4EA4\uFF0C\u8BF7\u91CD\u65B0\u6838\u5BF9\u540E\u7ED9\u51FA\u672C\u8F6E\u7ED3\u8BBA\u3002\u539F\u8F93\u5165\uFF1A
507
+ await new ContextContent(join2(input.sessionDir, "content")).input(
508
+ interrupted ? `\u6B64\u524D\u8FD9\u4E00\u8F6E\u6267\u884C\u4E2D\u65AD\u6216\u672A\u6210\u529F\uFF0C\u8BF7\u7ED3\u5408\u5DF2\u4FDD\u5B58\u7684\u4E0A\u4E0B\u6587\u7EE7\u7EED\u3002\u5DF2\u63D0\u4EA4\u64CD\u4F5C\u7684\u56DE\u6267\u4ECD\u6709\u6548\uFF0C\u4E0D\u8981\u91CD\u590D\u53D1\u9001\uFF1B\u6682\u5B58\u56DE\u7B54\u548C\u5B8C\u6210\u62A5\u544A\u5C1A\u672A\u63D0\u4EA4\uFF0C\u8BF7\u91CD\u65B0\u6838\u5BF9\u540E\u7ED9\u51FA\u672C\u8F6E\u7ED3\u8BBA\u3002\u539F\u8F93\u5165\uFF1A
302
509
  ${input.text}` : input.text
510
+ )
303
511
  );
304
512
  await native.waitForIdle();
305
513
  const last = native.messages.filter((message) => message.role === "assistant").at(-1);
@@ -8,9 +8,9 @@
8
8
 
9
9
  模型和密钥继续在现有接入点设置中配置。执行配置不能增加未注册工具或突破快照访问范围。`settleSeconds` 用于冷启动及后续版本维护的合并等待,从该成员最新一次有效发布开始计算。
10
10
 
11
- 会话整理使用独立的 `session-overview` 和 `session-handoff` 资源,不加载文件探索的共享 system/tool 提示词。前者自动生成标题和短摘要,后者按需生成详细交接说明;二者分别调用 SDK。配置仅包含版本、合并等待、单次请求时限与推理级别,首次执行冻结资源,自动重试沿用。详见 [会话整理](../../../../docs/session-analysis.md)。
11
+ 会话整理使用独立的 `session-overview` 和 `session-handoff` 资源,不加载文件探索的共享 system/tool 提示词。前者自动生成标题和短摘要,后者自动生成详细交接说明;二者分别调用 SDK。配置仅包含版本、合并等待、单次请求时限与推理级别,首次执行冻结资源,自动重试沿用。详见 [会话整理](../../../../docs/session-analysis.md)。
12
12
 
13
- `task-bootstrap` v10 同时负责首次整理和后续维护,`directory-analysis` v3 复用原生沙箱。整个 Pi 进程在 Linux Bubblewrap 沙箱中运行,挂载只读成员工作区和可选历史基线。原生 read/bash/grep/find/ls 不由此目录改写描述,只有 query_tasks 是 Teammy 自定义工具,用于查询已有任务。业务提示词追加到 Pi 原生系统提示;最终输出 upsert / archive 与任务级 needsConfirmation,不生成分析级 uncertainties。旧冻结作业保留快照工具协议。详见 [任务维护设计](../../../../docs/task-agent-maintenance-v1.md)。
13
+ `task-bootstrap` v12 同时负责首次整理和后续维护,`directory-analysis` v3 复用原生沙箱。整个 Pi 进程在 Linux Bubblewrap 沙箱中运行,挂载只读成员工作区和可选历史基线。原生 read/bash/grep/find/ls 不由此目录改写描述,只有 query_tasks 是 Teammy 自定义工具,用于查询已有任务。业务提示词追加到 Pi 原生系统提示;最终输出 upsert / archive 与任务级 needsConfirmation,不生成分析级 uncertainties。旧冻结作业保留快照工具协议。详见 [任务维护设计](../../../../docs/task-agent-maintenance-v1.md)。
14
14
 
15
15
  `thinkingLevel` 默认 off;设为其他级别时通过 Pi 的通用推理参数映射传给适配器,接入点需支持对应推理能力。上下文和输出限额由网关处理,Teammy 不配置或发送这些额度。
16
16
 
@@ -29,7 +29,7 @@
29
29
  - `query`:标题、目标、进展或来源路径的子串,不区分大小写。
30
30
  - `visibility`:`active`、`archived` 或 `all`。默认 `active`;指定 `id` 时默认 `all`。`active` 表示未归档,也包含已完成任务。
31
31
 
32
- 返回 `{ items, total, omitted, note? }`。`total` 为匹配总数,`items` 最多 40 项,超出时给出省略数与提示,可缩小查询范围。每项包含 `taskId`、`title`、`goal`、`status`、`progress`、`needsConfirmation`、`sources`、`active`;来源包含 `path`、`sha256`、`revision` 与 `syncedAt`。任务表达已合并保存的人工修订。
32
+ 返回 `{ items, total, omitted, note? }`。`total` 为匹配总数,`items` 最多 40 项,超出时给出省略数与提示,可缩小查询范围。每项包含 `taskId`、`revision`、`lastEdit`、`title`、`goal`、`status`、`progress`、`needsConfirmation`、`sources`、`active`;来源包含 `path`、`sha256`、`revision` 与 `syncedAt`。任务表达就是当前内容,直接包含用户修改;lastEdit 记录最近一次人工或委托修改的时间及操作者(旧数据可未知)。后续分析依据新证据继续维护,没有永久覆盖字段。
33
33
 
34
34
  当前数据范围是**作业创建时冻结的单成员任务**,不是实时的全项目查询;参数不支持项目或成员切换。调用范围由宿主提供的 `TaskContext` 和成员身份决定,工具自身不访问数据库,也没有同名 HTTP 路由。
35
35
 
@@ -17,6 +17,6 @@
17
17
  },
18
18
  "query_tasks": {
19
19
  "label": "查询已有任务",
20
- "description": "Query existing tasks of the current member by id, source (path relative to workspaces), or query (title/goal/progress/source substring). visibility=active (default), archived or all; an exact id defaults to all. Returns taskId, task fields, sources and active flag. Long results are abbreviated with an omitted count; narrow the query as needed."
20
+ "description": "Query existing tasks of the current member by id, source (path relative to workspaces), or query (title/goal/progress/source substring). visibility=active (default), archived or all; an exact id defaults to all. Returns taskId, current task fields, revision, lastEdit (latest human/delegated edit, nullable time and actor), sources and active flag. Continue from these current values, including user edits, using new evidence; do not reconstruct a separate automatic version from old files. Long results are abbreviated with an omitted count; narrow the query as needed."
21
21
  }
22
22
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": 2,
3
- "settleSeconds": 0,
2
+ "version": 3,
3
+ "settleSeconds": 60,
4
4
  "requestTimeoutMs": 300000,
5
5
  "thinkingLevel": "off"
6
6
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 11,
2
+ "version": 12,
3
3
  "thinkingLevel": "off",
4
4
  "tools": ["read", "bash", "grep", "find", "ls", "query_tasks"],
5
5
  "maxRounds": 256,
@@ -29,7 +29,7 @@
29
29
 
30
30
  - `title`:简洁的任务标题,普通进展变化时保持稳定。
31
31
  - `goal`:目标或完成边界,未明确时为 null。
32
- - `status`:planned(计划)、in_progress(进行中)、in_review(等待决策或验收)、completed(整体完成);无法判断时为 null。
32
+ - `status`:planned(计划)、in_progress(进行中)、in_review(待验收)、completed(整体完成);无法判断时为 null。待验收仅用于任务约定的成果已经具备并提交,正在等待验收或最终确认;前置审批、方案选型等过程中的决策,尚未开展工作时归计划,已开展调研、设计或实施时归进行中。按任务自身目标判断:实现功能前的方案评审属于过程,但“交付设计方案”任务的方案提交后可以待验收。仍有影响交付的实质工作时保持进行中;验收要求返工可回到进行中,不强制逐阶段流转。
33
33
  - `progress`:面向看板,优先用一两句话概括当前成果、所处阶段和影响完成的剩余事项;实现细节与无关统计保留在来源中。
34
34
  - `needsConfirmation`:资料的歧义是否影响对任务目标、状态或进展的理解,需要人类确认;是为 true,否则为 false。已明确的待办或等待条件本身不构成歧义。仍正常填写该任务的其他字段。
35
35
  - `sources`:支撑判断的当前成员文件,路径相对于 `/workspaces`,包含成员目录前缀。
@@ -42,7 +42,9 @@
42
42
 
43
43
  更新关注任务表达和来源的实质变化,无需为了措辞或附带统计的变化重写任务。
44
44
 
45
- 已有任务也可能由成员确认创建(origin 为 confirmed),来源中的 material 表示项目资料,不在本轮成员目录内。结合已有任务避免重复;不能因成员文件缺少对应记录而撤下这类任务。人工确认的字段由系统保留,本轮只补充工作区中的实际变化。
45
+ query_tasks 返回任务的当前内容,已经包含用户保存的修改,是本轮分析的起点;没有独立的自动结果或永久锁定字段。lastEdit 记录最近一次人工或委托修改的时间和操作者(旧记录可能未知),revision 用于版本保护。结合当前内容、历史基线及后续新证据继续更新标题、目标、状态、进展和 needsConfirmation;没有实质新证据时保留当前内容,不因重复读到旧资料就撤销用户的判断。后续验收通过可更新为 completed,新的返工记录可更新为 in_progress;新的资料歧义仍可将 needsConfirmation 设为 true。
46
+
47
+ 已有任务也可能由成员确认创建(origin 为 confirmed),来源中的 material 表示项目资料,不在本轮成员目录内。结合已有任务避免重复,这些任务同样可以根据后续证据更新。归档必须有明确的合并、替代或撤下依据,不因来源缺失、工作完成、本轮未涉及或任务由人工修改过就归档。
46
48
 
47
49
  ## 分析范围与复核
48
50