@nowcrew/daemon 0.6.87-beta.3 → 0.6.87-beta.5
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 +22 -0
- package/dist/cloud-report-uploader.js +27 -25
- package/dist/execution-runner.js +6 -3
- package/dist/external-output.js +154 -68
- package/dist/local-execution-prompt.js +19 -25
- package/dist/local-executor.js +16 -44
- package/dist/local-memory/prompt.js +37 -0
- package/dist/local-memory/read-cli.js +232 -0
- package/dist/local-memory-diagnostics.js +79 -12
- package/dist/main.js +6 -2
- package/dist/prompt.js +24 -9
- package/dist/serve.js +17 -11
- package/dist/storage-identity.js +4 -1
- package/dist/storage-ownership.js +93 -5
- package/dist/storage-runtime-controller.js +40 -20
- package/dist/storage-runtime.js +38 -2
- package/dist/storage-serve-adapter.js +22 -5
- package/dist/workspace.js +15 -0
- package/package.json +1 -1
- package/dist/project-skills/capability.js +0 -109
- package/dist/project-skills/controller-convergence.js +0 -57
- package/dist/project-skills/projection-set-switch.js +0 -419
- package/dist/project-skills/projection-state-domain.js +0 -170
- package/dist/project-skills/projection-state-store.js +0 -842
- package/dist/project-skills/projection-state-transaction.js +0 -322
- package/dist/project-skills/projection-state.js +0 -3
- package/dist/project-skills/runtime-root-bootstrap.js +0 -47
- package/dist/project-skills/runtime-root-domain.js +0 -268
- package/dist/project-skills/runtime-root-gc.js +0 -293
- package/dist/project-skills/runtime-root-lease-artifact.js +0 -46
- package/dist/project-skills/runtime-root-leases.js +0 -487
- package/dist/project-skills/runtime-root-source-identity.js +0 -60
- package/dist/project-skills/runtime-root-startup.js +0 -49
- package/dist/project-skills/runtime-root-state-artifact-domain.js +0 -143
- package/dist/project-skills/runtime-root-state-index.js +0 -356
- package/dist/project-skills/runtime-root-store.js +0 -722
- package/dist/project-skills/serve-capability.js +0 -28
- package/dist/project-skills/serve-startup.js +0 -37
package/README.md
CHANGED
|
@@ -418,6 +418,28 @@ Local policy can reduce server-requested access and limits; it cannot grant more
|
|
|
418
418
|
Provider credentials and configured environment are prepared locally and never carried in execution
|
|
419
419
|
control frames.
|
|
420
420
|
|
|
421
|
+
## 完整本地记忆读取
|
|
422
|
+
|
|
423
|
+
运行时注入的 `MEMORY.md` 是有界预览,未命中不代表完整文件没有相关记忆。
|
|
424
|
+
普通 Agent 工作区会在现有 PATH 目录中提供只读 `crew-memory` 工具,无需 Server 凭证:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
crew-memory search --query '提交前检查'
|
|
428
|
+
crew-memory search --query '提交前检查' --after-line 120
|
|
429
|
+
crew-memory read --offset 2400 --limit 2000
|
|
430
|
+
crew-memory read --file notes/testing.md --offset 0 --limit 2000
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
命令默认返回 JSON。搜索遍历完整指定文件,返回 `line`、`offset`、命中摘要和
|
|
434
|
+
`nextAfterLine`;读取按 Unicode 字符偏移分页,使用 `nextOffset` 继续直到 `eof=true`。
|
|
435
|
+
后续页可传 `--if-hash <上一页的sha256>`,文件变化时明确停止,不混读不同版本。
|
|
436
|
+
每页 JSON 有 12 KiB 上限,输入文件超过 32 MiB 时明确报错,不将未完成搜索伪装为零命中。
|
|
437
|
+
|
|
438
|
+
工具仅支持 `MEMORY.md` 和 `notes/` 下明确指定的 Markdown 文件,不遍历 pending,
|
|
439
|
+
不写入或迁移记忆。搜索结果是原始资料定位,读取明细前仍需确认当前有效索引和正文片段。
|
|
440
|
+
这些修改不改变现有 V2 写入上限、候选状态或归档策略,也不提供自动语义召回保证。
|
|
441
|
+
工具随 daemon 包发布;旧 daemon 没有该入口时,Agent 使用原生文件搜索与分段读取回退。
|
|
442
|
+
|
|
421
443
|
## Optional TencentDB Agent Memory Bridge
|
|
422
444
|
|
|
423
445
|
Execution protocol v1 can opt NowWork Agents into the private TencentDB Agent Memory Panel.
|
|
@@ -13,22 +13,17 @@ const fileFingerprint = async (path) => {
|
|
|
13
13
|
const stat = await lstat(path);
|
|
14
14
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
15
15
|
};
|
|
16
|
-
function isReportFile(root, path) {
|
|
16
|
+
function isReportFile(root, path, kind) {
|
|
17
17
|
const parts = relative(root, path).split(sep).filter(Boolean);
|
|
18
18
|
const filename = parts.at(-1) ?? "";
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
if (parts[0] !== "qa-report-server-v2")
|
|
23
|
-
return false;
|
|
24
|
-
if (parts[1] === "static" && parts.length === 3)
|
|
25
|
-
return extname(filename).toLowerCase() === ".html";
|
|
26
|
-
if (parts[1] === "qa-results" && parts.length >= 4) {
|
|
19
|
+
if (kind === "static")
|
|
20
|
+
return parts.length === 1 && extname(filename).toLowerCase() === ".html";
|
|
21
|
+
if (parts.length >= 2) {
|
|
27
22
|
return REPORT_FILENAMES.has(filename) || extname(filename).toLowerCase() === ".html";
|
|
28
23
|
}
|
|
29
24
|
return false;
|
|
30
25
|
}
|
|
31
|
-
async function walkReports(root, directory, depth, output) {
|
|
26
|
+
async function walkReports(root, directory, depth, output, kind) {
|
|
32
27
|
if (depth > MAX_DEPTH || output.length >= MAX_FILES)
|
|
33
28
|
return;
|
|
34
29
|
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
@@ -39,28 +34,26 @@ async function walkReports(root, directory, depth, output) {
|
|
|
39
34
|
continue;
|
|
40
35
|
const path = join(directory, entry.name);
|
|
41
36
|
if (entry.isDirectory()) {
|
|
42
|
-
await walkReports(root, path, depth + 1, output);
|
|
37
|
+
await walkReports(root, path, depth + 1, output, kind);
|
|
43
38
|
}
|
|
44
|
-
else if (entry.isFile() && isReportFile(root, path)) {
|
|
39
|
+
else if (entry.isFile() && isReportFile(root, path, kind)) {
|
|
45
40
|
output.push(resolve(path));
|
|
46
41
|
}
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
|
-
export async function snapshotReportFiles(root) {
|
|
44
|
+
export async function snapshotReportFiles(root, qaReportRoot) {
|
|
50
45
|
const files = [];
|
|
51
46
|
const workspaceRoot = resolve(root);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
join(
|
|
55
|
-
|
|
56
|
-
]) {
|
|
57
|
-
await walkReports(workspaceRoot, directory, 0, files);
|
|
47
|
+
const hub = resolve(qaReportRoot ?? join(workspaceRoot, "qa-report-server-v2"));
|
|
48
|
+
for (const kind of ["tasks", "qa-results", "static"]) {
|
|
49
|
+
const directory = join(kind === "tasks" ? workspaceRoot : hub, kind);
|
|
50
|
+
await walkReports(directory, directory, 0, files, kind);
|
|
58
51
|
}
|
|
59
52
|
const entries = await Promise.all(files.map(async (path) => [path, await fileFingerprint(path)]));
|
|
60
53
|
return new Map(entries);
|
|
61
54
|
}
|
|
62
|
-
export async function discoverReportFiles(root, before) {
|
|
63
|
-
const after = await snapshotReportFiles(root);
|
|
55
|
+
export async function discoverReportFiles(root, before, qaReportRoot) {
|
|
56
|
+
const after = await snapshotReportFiles(root, qaReportRoot);
|
|
64
57
|
return [...after.entries()]
|
|
65
58
|
.filter(([path, fingerprint]) => before.get(path) !== fingerprint)
|
|
66
59
|
.map(([path]) => path)
|
|
@@ -97,7 +90,7 @@ async function verifyOssUrl(url) {
|
|
|
97
90
|
}
|
|
98
91
|
}
|
|
99
92
|
export async function uploadCloudReports(input) {
|
|
100
|
-
const files = await discoverReportFiles(input.root, input.before);
|
|
93
|
+
const files = await discoverReportFiles(input.root, input.before, input.qaReportRoot);
|
|
101
94
|
if (files.length === 0)
|
|
102
95
|
return { urls: [], failures: [] };
|
|
103
96
|
const run = input.execFile ?? (async (file, args) => {
|
|
@@ -131,10 +124,10 @@ export async function uploadCloudReports(input) {
|
|
|
131
124
|
}
|
|
132
125
|
return { urls, failures };
|
|
133
126
|
}
|
|
134
|
-
export async function prepareCloudReportSnapshot(facts, root) {
|
|
127
|
+
export async function prepareCloudReportSnapshot(facts, root, qaReportRoot) {
|
|
135
128
|
if (!facts.enabled)
|
|
136
129
|
return null;
|
|
137
|
-
return snapshotReportFiles(root);
|
|
130
|
+
return snapshotReportFiles(root, qaReportRoot);
|
|
138
131
|
}
|
|
139
132
|
export async function prepareCloudReportExecution(input) {
|
|
140
133
|
const facts = cloudReportUploadFacts(input.env);
|
|
@@ -147,7 +140,7 @@ export async function prepareCloudReportExecution(input) {
|
|
|
147
140
|
return { facts, snapshot: null };
|
|
148
141
|
}
|
|
149
142
|
try {
|
|
150
|
-
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root) };
|
|
143
|
+
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root, input.qaReportRoot) };
|
|
151
144
|
}
|
|
152
145
|
catch (error) {
|
|
153
146
|
dslog("cloud_report_upload.snapshot_failed", "报告文件快照失败", {
|
|
@@ -177,6 +170,7 @@ export async function deliverCloudReports(input) {
|
|
|
177
170
|
const result = await (input.upload ?? uploadCloudReports)({
|
|
178
171
|
script: input.facts.ossUploadScript,
|
|
179
172
|
root: input.root,
|
|
173
|
+
...(input.qaReportRoot === undefined ? {} : { qaReportRoot: input.qaReportRoot }),
|
|
180
174
|
before: input.before,
|
|
181
175
|
});
|
|
182
176
|
if (result.failures.length > 0) {
|
|
@@ -185,6 +179,14 @@ export async function deliverCloudReports(input) {
|
|
|
185
179
|
uploaded_count: result.urls.length, failed_count: result.failures.length,
|
|
186
180
|
});
|
|
187
181
|
}
|
|
182
|
+
else {
|
|
183
|
+
dslog(result.urls.length === 0 ? "cloud_report_upload.no_files" : "cloud_report_upload.completed", result.urls.length === 0 ? "未发现新增或变化的报告文件" : "报告上传完成", {
|
|
184
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
185
|
+
uploaded_count: result.urls.length,
|
|
186
|
+
report_root: input.root,
|
|
187
|
+
qa_report_root: input.qaReportRoot ?? join(input.root, "qa-report-server-v2"),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
188
190
|
return {
|
|
189
191
|
finalText: input.captureFinal && result.urls.length > 0
|
|
190
192
|
? appendReportLinks(input.finalText ?? "", result.urls)
|
package/dist/execution-runner.js
CHANGED
|
@@ -834,9 +834,12 @@ export async function runExecution(config, input, dependencies) {
|
|
|
834
834
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
835
835
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
836
836
|
});
|
|
837
|
-
const boundedSystemPrompt = (context) =>
|
|
838
|
-
|
|
839
|
-
|
|
837
|
+
const boundedSystemPrompt = (context) => {
|
|
838
|
+
const base = typeof systemPromptWithLocalFacts === "string"
|
|
839
|
+
? systemPromptWithLocalFacts
|
|
840
|
+
: systemPromptWithLocalFacts(context);
|
|
841
|
+
return appendAgentMemoryContext(base, recalledMemory, Math.max(Buffer.byteLength(base, "utf8"), baseSystemPromptBudget - (context.reservedPromptBytes ?? 0)));
|
|
842
|
+
};
|
|
840
843
|
const localInput = {
|
|
841
844
|
executionId: spec.executionId,
|
|
842
845
|
handle: spec.agent.handle,
|
package/dist/external-output.js
CHANGED
|
@@ -1,15 +1,104 @@
|
|
|
1
1
|
export const EXTERNAL_ANSWER_OPEN = "<nowwork_external_answer>";
|
|
2
2
|
export const EXTERNAL_ANSWER_CLOSE = "</nowwork_external_answer>";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const MAX_MARKER_TAG_LENGTH = 96;
|
|
4
|
+
const KNOWN_EXTERNAL_ANSWER_TAG = /^now+orkexternalanswer$/u;
|
|
5
|
+
const CANONICAL_EXTERNAL_ANSWER_TAG = "noworkexternalanswer";
|
|
6
|
+
function editDistanceWithin(value, target, maximum) {
|
|
7
|
+
if (Math.abs(value.length - target.length) > maximum)
|
|
8
|
+
return false;
|
|
9
|
+
let previous = Array.from({ length: target.length + 1 }, (_, index) => index);
|
|
10
|
+
for (let row = 1; row <= value.length; row += 1) {
|
|
11
|
+
const current = [row];
|
|
12
|
+
let rowMinimum = row;
|
|
13
|
+
for (let column = 1; column <= target.length; column += 1) {
|
|
14
|
+
const cost = value[row - 1] === target[column - 1] ? 0 : 1;
|
|
15
|
+
const distance = Math.min(previous[column] + 1, current[column - 1] + 1, previous[column - 1] + cost);
|
|
16
|
+
current.push(distance);
|
|
17
|
+
rowMinimum = Math.min(rowMinimum, distance);
|
|
18
|
+
}
|
|
19
|
+
if (rowMinimum > maximum)
|
|
20
|
+
return false;
|
|
21
|
+
previous = current;
|
|
22
|
+
}
|
|
23
|
+
return previous[target.length] <= maximum;
|
|
24
|
+
}
|
|
25
|
+
function resemblesExternalAnswerTag(name) {
|
|
26
|
+
if (name.includes("externalanswer"))
|
|
27
|
+
return true;
|
|
28
|
+
const canonicalized = name.replace(/^now+ork/u, "nowork");
|
|
29
|
+
return editDistanceWithin(canonicalized, CANONICAL_EXTERNAL_ANSWER_TAG, 2);
|
|
30
|
+
}
|
|
31
|
+
function classifyMarkerTag(body) {
|
|
32
|
+
const trimmed = body.trim();
|
|
33
|
+
const closing = trimmed.startsWith("/");
|
|
34
|
+
const name = (closing ? trimmed.slice(1) : trimmed)
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[_\-\s]/gu, "");
|
|
38
|
+
if (KNOWN_EXTERNAL_ANSWER_TAG.test(name))
|
|
39
|
+
return { closing, kind: "known" };
|
|
40
|
+
return {
|
|
41
|
+
closing,
|
|
42
|
+
kind: resemblesExternalAnswerTag(name) ? "suspicious" : "other",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function nextMarkerTag(value, from) {
|
|
46
|
+
let cursor = from;
|
|
47
|
+
while (cursor < value.length) {
|
|
48
|
+
const start = value.indexOf("<", cursor);
|
|
49
|
+
if (start < 0)
|
|
50
|
+
return null;
|
|
51
|
+
const end = value.indexOf(">", start + 1);
|
|
52
|
+
if (end < 0)
|
|
53
|
+
return null;
|
|
54
|
+
const body = value.slice(start + 1, end);
|
|
55
|
+
const classified = classifyMarkerTag(body);
|
|
56
|
+
if (classified.kind === "suspicious"
|
|
57
|
+
|| (classified.kind === "known" && body.length > MAX_MARKER_TAG_LENGTH)) {
|
|
58
|
+
return { start, end: end + 1, closing: classified.closing, kind: "suspicious" };
|
|
59
|
+
}
|
|
60
|
+
if (classified.kind === "known") {
|
|
61
|
+
return { start, end: end + 1, closing: classified.closing, kind: "known" };
|
|
62
|
+
}
|
|
63
|
+
cursor = end + 1;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function parseExternalAnswerText(value) {
|
|
68
|
+
const sections = [];
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
let contentStart = 0;
|
|
71
|
+
let inside = false;
|
|
72
|
+
let found = false;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const tag = nextMarkerTag(value, cursor);
|
|
75
|
+
if (!tag)
|
|
76
|
+
break;
|
|
77
|
+
if (tag.kind === "suspicious")
|
|
78
|
+
return { kind: "rejected", text: "" };
|
|
79
|
+
if (tag.closing) {
|
|
80
|
+
if (!inside)
|
|
81
|
+
return { kind: "rejected", text: "" };
|
|
82
|
+
sections.push(value.slice(contentStart, tag.start));
|
|
83
|
+
inside = false;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (inside)
|
|
87
|
+
return { kind: "rejected", text: "" };
|
|
88
|
+
found = true;
|
|
89
|
+
inside = true;
|
|
90
|
+
contentStart = tag.end;
|
|
91
|
+
}
|
|
92
|
+
cursor = tag.end;
|
|
8
93
|
}
|
|
9
|
-
|
|
94
|
+
if (inside)
|
|
95
|
+
sections.push(value.slice(contentStart));
|
|
96
|
+
if (!found)
|
|
97
|
+
return { kind: "plain", text: value.trim() };
|
|
98
|
+
return { kind: "marked", text: sections.join("").trim() };
|
|
10
99
|
}
|
|
11
100
|
/**
|
|
12
|
-
* 跨 chunk
|
|
101
|
+
* 跨 chunk 的契约标签解码器:push() 在合法闭合后返回完整标签块(供 answerStream 转发);
|
|
13
102
|
* 同时记住整轮里**最后一个完整闭合**的标签块(`lastClosedBlock`),供完结时作为契约答案——
|
|
14
103
|
* 只有闭合了的块才算数:半截块(被工具调用拆开的前半段)与叙述里提到的开标签
|
|
15
104
|
* ("我会把结论包在 <marker> 里,先去跑测试")都不会被当成答案(review of a027bf15, C1)。
|
|
@@ -40,34 +129,63 @@ export class ExternalAnswerDecoder {
|
|
|
40
129
|
}
|
|
41
130
|
consume(value, output) {
|
|
42
131
|
this.pending = "";
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
132
|
+
const appendInside = (text) => {
|
|
133
|
+
if (!text)
|
|
134
|
+
return;
|
|
135
|
+
this.block += text;
|
|
136
|
+
};
|
|
137
|
+
for (const character of value) {
|
|
138
|
+
if (this.pending) {
|
|
139
|
+
this.pending += character;
|
|
140
|
+
if (character === ">") {
|
|
141
|
+
const body = this.pending.slice(1, -1);
|
|
142
|
+
const tag = classifyMarkerTag(body);
|
|
143
|
+
if (tag.kind === "known" && body.length <= MAX_MARKER_TAG_LENGTH) {
|
|
144
|
+
if (tag.closing && this.state === "inside") {
|
|
145
|
+
const completed = this.block.trim();
|
|
146
|
+
if (completed) {
|
|
147
|
+
this.closed = completed;
|
|
148
|
+
output.push(completed);
|
|
149
|
+
}
|
|
150
|
+
this.block = "";
|
|
151
|
+
this.state = "outside";
|
|
152
|
+
}
|
|
153
|
+
else if (!tag.closing && this.state === "outside") {
|
|
154
|
+
this.block = "";
|
|
155
|
+
this.state = "inside";
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.block = "";
|
|
159
|
+
this.state = tag.closing ? "outside" : "inside";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (tag.kind === "suspicious" || tag.kind === "known") {
|
|
163
|
+
this.block = "";
|
|
164
|
+
this.state = "outside";
|
|
165
|
+
}
|
|
166
|
+
else if (this.state === "inside") {
|
|
167
|
+
appendInside(this.pending);
|
|
168
|
+
}
|
|
169
|
+
this.pending = "";
|
|
170
|
+
}
|
|
171
|
+
else if (this.pending.length > MAX_MARKER_TAG_LENGTH + 2) {
|
|
172
|
+
const tag = classifyMarkerTag(this.pending.slice(1));
|
|
173
|
+
if (tag.kind === "suspicious" || tag.kind === "known") {
|
|
174
|
+
this.block = "";
|
|
175
|
+
this.state = "outside";
|
|
176
|
+
}
|
|
177
|
+
else if (this.state === "inside") {
|
|
178
|
+
appendInside(this.pending);
|
|
179
|
+
}
|
|
180
|
+
this.pending = "";
|
|
181
|
+
}
|
|
55
182
|
}
|
|
56
|
-
else {
|
|
57
|
-
this.
|
|
58
|
-
|
|
183
|
+
else if (character === "<") {
|
|
184
|
+
this.pending = character;
|
|
185
|
+
}
|
|
186
|
+
else if (this.state === "inside") {
|
|
187
|
+
appendInside(character);
|
|
59
188
|
}
|
|
60
|
-
const remaining = value.slice(markerAt + marker.length);
|
|
61
|
-
if (remaining)
|
|
62
|
-
this.consume(remaining, output);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const retained = retainedMarkerPrefix(value, marker);
|
|
66
|
-
const safe = retained > 0 ? value.slice(0, -retained) : value;
|
|
67
|
-
this.pending = retained > 0 ? value.slice(-retained) : "";
|
|
68
|
-
if (this.state === "inside" && safe) {
|
|
69
|
-
output.push(safe);
|
|
70
|
-
this.block += safe;
|
|
71
189
|
}
|
|
72
190
|
}
|
|
73
191
|
}
|
|
@@ -104,20 +222,7 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
|
104
222
|
: [];
|
|
105
223
|
}
|
|
106
224
|
export function stripExternalAnswerMarkers(value) {
|
|
107
|
-
|
|
108
|
-
let cursor = 0;
|
|
109
|
-
while (cursor < value.length) {
|
|
110
|
-
const openAt = value.indexOf(EXTERNAL_ANSWER_OPEN, cursor);
|
|
111
|
-
if (openAt < 0)
|
|
112
|
-
break;
|
|
113
|
-
const contentAt = openAt + EXTERNAL_ANSWER_OPEN.length;
|
|
114
|
-
const closeAt = value.indexOf(EXTERNAL_ANSWER_CLOSE, contentAt);
|
|
115
|
-
if (closeAt < 0)
|
|
116
|
-
return value.slice(contentAt).trim();
|
|
117
|
-
sections.push(value.slice(contentAt, closeAt));
|
|
118
|
-
cursor = closeAt + EXTERNAL_ANSWER_CLOSE.length;
|
|
119
|
-
}
|
|
120
|
-
return (sections.length > 0 ? sections.join("") : value).trim();
|
|
225
|
+
return parseExternalAnswerText(value).text;
|
|
121
226
|
}
|
|
122
227
|
/**
|
|
123
228
|
* 提取 marker 内容:有 marker 返回拼接内容(trim,空→null),无 marker 返回 null。
|
|
@@ -125,25 +230,6 @@ export function stripExternalAnswerMarkers(value) {
|
|
|
125
230
|
* 本函数用于判定"agent 是否给出了频道直接回复"——必须能区分有无 marker。
|
|
126
231
|
*/
|
|
127
232
|
export function extractExternalAnswer(value) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
let rest = value;
|
|
131
|
-
for (;;) {
|
|
132
|
-
const open = rest.indexOf(EXTERNAL_ANSWER_OPEN);
|
|
133
|
-
if (open < 0)
|
|
134
|
-
break;
|
|
135
|
-
found = true;
|
|
136
|
-
const afterOpen = rest.slice(open + EXTERNAL_ANSWER_OPEN.length);
|
|
137
|
-
const close = afterOpen.indexOf(EXTERNAL_ANSWER_CLOSE);
|
|
138
|
-
if (close < 0) {
|
|
139
|
-
parts.push(afterOpen);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
parts.push(afterOpen.slice(0, close));
|
|
143
|
-
rest = afterOpen.slice(close + EXTERNAL_ANSWER_CLOSE.length);
|
|
144
|
-
}
|
|
145
|
-
if (!found)
|
|
146
|
-
return null;
|
|
147
|
-
const answer = parts.join("").trim();
|
|
148
|
-
return answer.length > 0 ? answer : null;
|
|
233
|
+
const parsed = parseExternalAnswerText(value);
|
|
234
|
+
return parsed.kind === "marked" && parsed.text ? parsed.text : null;
|
|
149
235
|
}
|
|
@@ -34,45 +34,39 @@ export function truncateUtf8(value, maxBytes) {
|
|
|
34
34
|
}
|
|
35
35
|
return value.slice(0, end);
|
|
36
36
|
}
|
|
37
|
-
function takeCompletePromptSections(sections, maxBytes) {
|
|
38
|
-
return sections.reduce((state, section) => {
|
|
39
|
-
if (!state.full)
|
|
40
|
-
return state;
|
|
41
|
-
const sectionBytes = Buffer.byteLength(section, "utf8");
|
|
42
|
-
if (state.bytes + sectionBytes > maxBytes)
|
|
43
|
-
return { ...state, full: false };
|
|
44
|
-
return { value: `${state.value}${section}`, bytes: state.bytes + sectionBytes, full: true };
|
|
45
|
-
}, { value: "", bytes: 0, full: true }).value;
|
|
46
|
-
}
|
|
47
37
|
/** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
|
|
48
38
|
export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
|
|
49
|
-
return ({ workspace, resuming, projectContext, trainingProjectContext }) => {
|
|
50
|
-
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory);
|
|
39
|
+
return ({ workspace, resuming, projectContext, trainingProjectContext, reservedPromptBytes = 0 }) => {
|
|
51
40
|
const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
|
|
52
41
|
const collaborationPrompt = options.collaborationTools === true ? buildCollaborationToolsPrompt() : "";
|
|
53
42
|
const mandatoryWorkspacePrompt = `${buildRuntimeWorkspacePrompt(projectContext)}${buildTrainingGitProjectPrompt(trainingProjectContext)}`;
|
|
54
43
|
const crewHome = options.isolateLocalMemory ? workspace.runDir : workspace.dir;
|
|
55
|
-
const
|
|
44
|
+
const localFacts = `\n\n## Local execution facts
|
|
56
45
|
- $CREW_HOME: ${crewHome}
|
|
57
|
-
- $CREW_TASK_LOG: ${workspace.workLogPath}
|
|
58
|
-
...(memory ? [`\n\n## Injected MEMORY.md (bounded local context)\n${memory}`] : []),
|
|
59
|
-
...(workLog ? [`\n\n## Injected work-log (bounded local context)\n${workLog}`] : []),
|
|
60
|
-
];
|
|
46
|
+
- $CREW_TASK_LOG: ${workspace.workLogPath}`;
|
|
61
47
|
const serverPromptBytes = Buffer.byteLength(serverPrompt, "utf8");
|
|
62
48
|
const collaborationBytes = Buffer.byteLength(collaborationPrompt, "utf8");
|
|
63
49
|
const mandatoryBytes = Buffer.byteLength(mandatoryWorkspacePrompt, "utf8");
|
|
64
50
|
const requiredBytes = serverPromptBytes + collaborationBytes + mandatoryBytes;
|
|
65
|
-
//
|
|
51
|
+
// 先保留全部必需内容,再按实际剩余字节一次性构造预览。
|
|
66
52
|
if (collaborationPrompt !== "" && requiredBytes > maxBytes) {
|
|
67
53
|
throw new Error("collaboration_tools_prompt_budget_exceeded");
|
|
68
54
|
}
|
|
69
|
-
if (mandatoryWorkspacePrompt !== "") {
|
|
70
|
-
|
|
71
|
-
throw new Error("project_workspace_prompt_budget_exceeded");
|
|
72
|
-
const optionalFacts = takeCompletePromptSections(localFactSections, maxBytes - requiredBytes);
|
|
73
|
-
return `${serverPrompt}${collaborationPrompt}${optionalFacts}${mandatoryWorkspacePrompt}`;
|
|
55
|
+
if (mandatoryWorkspacePrompt !== "" && requiredBytes > maxBytes) {
|
|
56
|
+
throw new Error("project_workspace_prompt_budget_exceeded");
|
|
74
57
|
}
|
|
75
|
-
const remaining = maxBytes - requiredBytes;
|
|
76
|
-
|
|
58
|
+
const remaining = maxBytes - requiredBytes - Math.max(0, reservedPromptBytes);
|
|
59
|
+
const factsBytes = Buffer.byteLength(localFacts, "utf8");
|
|
60
|
+
if (factsBytes > remaining)
|
|
61
|
+
return `${serverPrompt}${collaborationPrompt}${mandatoryWorkspacePrompt}`;
|
|
62
|
+
const memoryHeading = "\n\n## Injected MEMORY.md (bounded local context)\n";
|
|
63
|
+
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"));
|
|
64
|
+
const memorySection = memory === "" ? "" : memoryHeading + memory;
|
|
65
|
+
const workLogSection = workLog === "" ? "" : `\n\n## Injected work-log (bounded local context)\n${workLog}`;
|
|
66
|
+
const workLogBudget = remaining - factsBytes - Buffer.byteLength(memorySection, "utf8");
|
|
67
|
+
const boundedWorkLog = mandatoryWorkspacePrompt === ""
|
|
68
|
+
? truncateUtf8(workLogSection, workLogBudget)
|
|
69
|
+
: Buffer.byteLength(workLogSection, "utf8") <= workLogBudget ? workLogSection : "";
|
|
70
|
+
return `${serverPrompt}${collaborationPrompt}${localFacts}${memorySection}${boundedWorkLog}${mandatoryWorkspacePrompt}`;
|
|
77
71
|
};
|
|
78
72
|
}
|
package/dist/local-executor.js
CHANGED
|
@@ -36,8 +36,8 @@ import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
|
36
36
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
37
|
import { machinePackageManagerEnv, packageManagerEnvFacts } from "./package-manager-env.js";
|
|
38
38
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
|
-
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath,
|
|
40
|
-
import {
|
|
39
|
+
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
40
|
+
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
41
41
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
42
42
|
constructor(runtime, timeoutMs) {
|
|
43
43
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -254,7 +254,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
254
254
|
let executionWorkspace = workspace;
|
|
255
255
|
let localMemoryTelemetry = null;
|
|
256
256
|
const inheritedEnv = { ...process.env };
|
|
257
|
-
const
|
|
257
|
+
const reportRoots = {
|
|
258
|
+
root: workspace.dir,
|
|
259
|
+
...(input.launch.qaReportRoot === undefined ? {} : { qaReportRoot: input.launch.qaReportRoot }),
|
|
260
|
+
};
|
|
261
|
+
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
258
262
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
259
263
|
for (const key of Object.keys(inheritedEnv)) {
|
|
260
264
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
@@ -382,26 +386,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
382
386
|
}
|
|
383
387
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
384
388
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
385
|
-
let pendingMemoryContext = "";
|
|
386
|
-
if (input.localMemoryV2Enabled === true) {
|
|
387
|
-
try {
|
|
388
|
-
const pendingCandidates = await searchPendingMemoryCandidates({
|
|
389
|
-
agentDir: executionWorkspace.dir,
|
|
390
|
-
query: wakePrompt,
|
|
391
|
-
limit: 5,
|
|
392
|
-
});
|
|
393
|
-
pendingMemoryContext = renderPendingMemoryContext(pendingCandidates);
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
397
|
-
level: "WARN",
|
|
398
|
-
execution_id: input.executionId,
|
|
399
|
-
agent_handle: input.handle,
|
|
400
|
-
error_name: error instanceof Error ? error.name : "unknown",
|
|
401
|
-
error_code: error?.code,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
389
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
406
390
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
407
391
|
&& isLocalMemoryV2Eligible({
|
|
@@ -413,26 +397,14 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
413
397
|
})) {
|
|
414
398
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
415
399
|
}
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<= input.maxSystemPromptBytes;
|
|
425
|
-
// The structured prune contract is mandatory for V2. Drop optional archived
|
|
426
|
-
// recall before dropping the daemon result file when the prompt budget is tight.
|
|
427
|
-
if (!fitsPromptBudget(baseSystemPrompt) && pendingMemoryContext !== "") {
|
|
428
|
-
baseSystemPrompt = promptWithoutPending;
|
|
429
|
-
}
|
|
430
|
-
const structuredPromptFits = fitsPromptBudget(baseSystemPrompt);
|
|
431
|
-
if (!structuredPromptFits)
|
|
432
|
-
localMemoryResultPath = null;
|
|
433
|
-
const systemPrompt = localMemoryResultPath === null
|
|
434
|
-
? baseSystemPrompt
|
|
435
|
-
: `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
|
|
400
|
+
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
401
|
+
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
402
|
+
enabled: input.localMemoryV2Enabled === true, query: wakePrompt,
|
|
403
|
+
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
404
|
+
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
405
|
+
});
|
|
406
|
+
localMemoryResultPath = memoryPrompt.resultPath;
|
|
407
|
+
const systemPrompt = memoryPrompt.systemPrompt;
|
|
436
408
|
if (!isolatedMaintenance)
|
|
437
409
|
try {
|
|
438
410
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -1018,7 +990,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1018
990
|
}
|
|
1019
991
|
const cloudReportResult = exitCode === 0
|
|
1020
992
|
? await deliverCloudReports({
|
|
1021
|
-
facts: cloudReportUpload,
|
|
993
|
+
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
1022
994
|
externalAnswer: externalOutput.lastClosedBlock
|
|
1023
995
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
1024
996
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { dslog } from "../slog.js";
|
|
2
|
+
import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
|
|
3
|
+
import { renderLocalMemoryResultInstructions } from "./runtime.js";
|
|
4
|
+
export async function prepareLocalMemoryPrompt(input) {
|
|
5
|
+
let pending = "";
|
|
6
|
+
if (input.enabled) {
|
|
7
|
+
try {
|
|
8
|
+
pending = renderPendingMemoryContext(await searchPendingMemoryCandidates({
|
|
9
|
+
agentDir: input.agentDir, query: input.query, limit: 5,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
14
|
+
level: "WARN",
|
|
15
|
+
execution_id: input.executionId,
|
|
16
|
+
agent_handle: input.agentHandle,
|
|
17
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
18
|
+
error_code: error?.code,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const original = input.renderBase(0);
|
|
23
|
+
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath);
|
|
24
|
+
const fits = (prompt) => input.maxBytes === undefined || Buffer.byteLength(prompt, "utf8") <= input.maxBytes;
|
|
25
|
+
const render = (suffix) => (suffix === "" ? original : input.renderBase(Buffer.byteLength(suffix, "utf8"))) + suffix;
|
|
26
|
+
// 后缀先占预算,可选预览重建;结构化契约优先于 pending,不能被预览挤掉。
|
|
27
|
+
const complete = render(pending + structured);
|
|
28
|
+
if (fits(complete))
|
|
29
|
+
return { systemPrompt: complete, resultPath: input.resultPath };
|
|
30
|
+
if (pending !== "") {
|
|
31
|
+
const withoutPending = render(structured);
|
|
32
|
+
if (fits(withoutPending))
|
|
33
|
+
return { systemPrompt: withoutPending, resultPath: input.resultPath };
|
|
34
|
+
}
|
|
35
|
+
// 强制基础指令与契约本身仍无法共存时,保留既有非结构化兼容回退,不裁基础指令。
|
|
36
|
+
return { systemPrompt: original, resultPath: null };
|
|
37
|
+
}
|