@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.
- package/README.md +1 -1
- package/dist/main.js +3190 -2575
- package/dist/native-pi-entry.js +211 -6
- package/dist/project-pi-entry.js +219 -11
- package/dist/runtime/README.md +3 -3
- package/dist/runtime/shared/tools.json +1 -1
- package/dist/runtime/workflows/session-handoff/config.json +2 -2
- package/dist/runtime/workflows/task-bootstrap/config.json +1 -1
- package/dist/runtime/workflows/task-bootstrap/prompt.md +4 -2
- package/dist/web/assets/{Assistant-o-N3hQtn.js → Assistant-Bdd5Nsup.js} +1 -1
- package/dist/web/assets/Dashboard-Bkdiv8g6.js +1 -0
- package/dist/web/assets/{Files-BvHbF1eW.js → Files-DrkZfsY3.js} +1 -1
- package/dist/web/assets/{HtmlPreview-Uzm5MRkj.js → HtmlPreview-BrlipR_1.js} +1 -1
- package/dist/web/assets/{Markdown-SFOHcUZw.js → Markdown-VmpJdXUC.js} +1 -1
- package/dist/web/assets/{Materials-DjvULSPf.js → Materials-CirZh-et.js} +1 -1
- package/dist/web/assets/Sessions-C88uXxYK.js +1 -0
- package/dist/web/assets/{WorkspaceTree-B7mZepdd.js → WorkspaceTree-DiWj8PhE.js} +1 -1
- package/dist/web/assets/{file-links-BHeuStKI.js → file-links-Dpr2sN4y.js} +1 -1
- package/dist/web/assets/{index-BW6ule39.js → index-BTcnf4jy.js} +3 -3
- package/dist/web/assets/project-links-Dy6-B89_.js +1 -0
- package/dist/web/downloads/teammy-skill.zip +0 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/Dashboard-BkoNYNTq.js +0 -1
- package/dist/web/assets/Sessions-BbxEbr9w.js +0 -1
- package/dist/web/assets/project-links-XbF78Bal.js +0 -1
package/dist/native-pi-entry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// apps/server/ai/native-pi-entry.ts
|
|
2
|
-
import { InMemoryCredentialStore, InMemoryModelsStore, Type as
|
|
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
|
|
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:
|
|
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.");
|
package/dist/project-pi-entry.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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);
|
package/dist/runtime/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
模型和密钥继续在现有接入点设置中配置。执行配置不能增加未注册工具或突破快照访问范围。`settleSeconds` 用于冷启动及后续版本维护的合并等待,从该成员最新一次有效发布开始计算。
|
|
10
10
|
|
|
11
|
-
会话整理使用独立的 `session-overview` 和 `session-handoff` 资源,不加载文件探索的共享 system/tool
|
|
11
|
+
会话整理使用独立的 `session-overview` 和 `session-handoff` 资源,不加载文件探索的共享 system/tool 提示词。前者自动生成标题和短摘要,后者自动生成详细交接说明;二者分别调用 SDK。配置仅包含版本、合并等待、单次请求时限与推理级别,首次执行冻结资源,自动重试沿用。详见 [会话整理](../../../../docs/session-analysis.md)。
|
|
12
12
|
|
|
13
|
-
`task-bootstrap`
|
|
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
|
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
- `title`:简洁的任务标题,普通进展变化时保持稳定。
|
|
31
31
|
- `goal`:目标或完成边界,未明确时为 null。
|
|
32
|
-
- `status`:planned(计划)、in_progress(进行中)、in_review
|
|
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
|
-
|
|
45
|
+
query_tasks 返回任务的当前内容,已经包含用户保存的修改,是本轮分析的起点;没有独立的自动结果或永久锁定字段。lastEdit 记录最近一次人工或委托修改的时间和操作者(旧记录可能未知),revision 用于版本保护。结合当前内容、历史基线及后续新证据继续更新标题、目标、状态、进展和 needsConfirmation;没有实质新证据时保留当前内容,不因重复读到旧资料就撤销用户的判断。后续验收通过可更新为 completed,新的返工记录可更新为 in_progress;新的资料歧义仍可将 needsConfirmation 设为 true。
|
|
46
|
+
|
|
47
|
+
已有任务也可能由成员确认创建(origin 为 confirmed),来源中的 material 表示项目资料,不在本轮成员目录内。结合已有任务避免重复,这些任务同样可以根据后续证据更新。归档必须有明确的合并、替代或撤下依据,不因来源缺失、工作完成、本轮未涉及或任务由人工修改过就归档。
|
|
46
48
|
|
|
47
49
|
## 分析范围与复核
|
|
48
50
|
|