@tea-agent/loop-agent 0.34.3 → 0.34.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/AGENTS.md +8 -3
- package/CHANGELOG.md +66 -22
- package/README.md +2 -2
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/worker/cli.js +21 -27
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +134 -4
- package/dist/worker/console/chat/pi-runtime.js +308 -63
- package/dist/worker/console/chat/resource-preferences-store.js +152 -0
- package/dist/worker/console/chat/routes.js +425 -19
- package/dist/worker/console/chat/shortcuts.js +208 -9
- package/dist/worker/console/chat/tool-preview.js +162 -5
- package/dist/worker/console/chat/turn-execution-registry.js +82 -0
- package/dist/worker/console/dag-execution-receipt.js +14 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/index.js +1 -0
- package/dist/worker/console/open-browser.js +134 -0
- package/dist/worker/console/recovery-cta.js +1 -1
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/index-qpkysQYW.css +1 -0
- package/dist/worker/console/static/assets/index-y980PqtP.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-markdown-security.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +1 -3
- package/dist/worker/console/static-src/operator-chat/format.js +2 -2
- package/dist/worker/console/static-src/operator-chat/refs.js +24 -0
- package/dist/worker/console/static-src/operator-chat/resource-auto-invocation.js +91 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +690 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +158 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +535 -86
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +81 -3
- package/dist/workflows/dag/init-hybrid.js +2 -0
- package/docs/architecture/evolution.md +1 -1
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/operations/local-development-environment.md +4 -2
- package/docs/templates/branch-merge-report.md +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
- package/docs/templates/init-managed-agents.md +2 -2
- package/package.json +6 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +2 -2
- package/skills/loop-agent/references/command-reference.md +2 -3
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +0 -1
- package/dist/worker/console/static/assets/index-BpuHmlSP.js +0 -29
|
@@ -1,16 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat slash command catalog (UI-11).
|
|
3
|
+
*
|
|
4
|
+
* Local Operator + Pi Web commands are always available. Dynamic extension /
|
|
5
|
+
* prompt / skill commands are merged via mergeSlashCommands with conflict
|
|
6
|
+
* priority: operator/web > extension > skill > prompt.
|
|
7
|
+
*/
|
|
1
8
|
export const SHORTCUTS = [
|
|
2
|
-
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
{
|
|
10
|
+
command: "/task",
|
|
11
|
+
label: "Task context",
|
|
12
|
+
description: "查看当前任务、Draft 与 assessment",
|
|
13
|
+
action: "focus-panel:task",
|
|
14
|
+
source: "operator",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
command: "/contract",
|
|
18
|
+
label: "Contract",
|
|
19
|
+
description: "查看 contract diff 与 apply gate",
|
|
20
|
+
action: "focus-panel:contract",
|
|
21
|
+
source: "operator",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: "/dag",
|
|
25
|
+
label: "DAG",
|
|
26
|
+
description: "查看 DAG spine、确认与运行状态",
|
|
27
|
+
action: "focus-panel:dag",
|
|
28
|
+
source: "operator",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
command: "/interview",
|
|
32
|
+
label: "Interview",
|
|
33
|
+
description: "打开 Requirement Interview",
|
|
34
|
+
action: "focus:interview",
|
|
35
|
+
source: "operator",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
command: "/reload",
|
|
39
|
+
label: "Reload session",
|
|
40
|
+
description: "重新加载当前 Pi 会话的设置、扩展、技能、提示词、主题与上下文文件(不产生消息)",
|
|
41
|
+
action: "reload",
|
|
42
|
+
source: "operator",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: "/help",
|
|
46
|
+
label: "Shortcuts",
|
|
47
|
+
description: "显示可用 shortcut",
|
|
48
|
+
action: "show-help",
|
|
49
|
+
source: "operator",
|
|
50
|
+
},
|
|
8
51
|
];
|
|
9
|
-
/**
|
|
52
|
+
/** Pi Web commands with real browser-equivalent behavior (UI-11). */
|
|
53
|
+
export const WEB_SLASH_COMMANDS = [
|
|
54
|
+
{
|
|
55
|
+
command: "/compact",
|
|
56
|
+
label: "压缩上下文",
|
|
57
|
+
description: "压缩当前会话上下文,释放 token",
|
|
58
|
+
action: "compact",
|
|
59
|
+
source: "web",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
command: "/name",
|
|
63
|
+
label: "重命名会话",
|
|
64
|
+
description: "设置当前会话标题",
|
|
65
|
+
action: "name",
|
|
66
|
+
source: "web",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
command: "/session",
|
|
70
|
+
label: "会话信息",
|
|
71
|
+
description: "显示当前会话元信息",
|
|
72
|
+
action: "session",
|
|
73
|
+
source: "web",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
command: "/copy",
|
|
77
|
+
label: "复制回复",
|
|
78
|
+
description: "复制最后一条助手消息原文",
|
|
79
|
+
action: "copy",
|
|
80
|
+
source: "web",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const SOURCE_PRIORITY = {
|
|
84
|
+
operator: 0,
|
|
85
|
+
web: 1,
|
|
86
|
+
extension: 2,
|
|
87
|
+
skill: 3,
|
|
88
|
+
prompt: 4,
|
|
89
|
+
};
|
|
90
|
+
function normalizeCommandName(raw) {
|
|
91
|
+
const token = raw.trim().split(/\s+/, 1)[0] ?? "";
|
|
92
|
+
if (!token)
|
|
93
|
+
return "";
|
|
94
|
+
return token.startsWith("/") ? token.toLowerCase() : `/${token.toLowerCase()}`;
|
|
95
|
+
}
|
|
96
|
+
/** Pure navigation/display resolver for local Operator + Web commands. */
|
|
10
97
|
export function resolveShortcut(input) {
|
|
11
98
|
const token = input.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
12
99
|
if (!token?.startsWith("/"))
|
|
13
100
|
return undefined;
|
|
14
|
-
const
|
|
15
|
-
|
|
101
|
+
const local = [...SHORTCUTS, ...WEB_SLASH_COMMANDS].find((shortcut) => shortcut.command === token);
|
|
102
|
+
if (local)
|
|
103
|
+
return { command: local.command, action: local.action };
|
|
104
|
+
return { command: token, action: "unknown" };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Merge Operator, Web, and dynamic Session commands with conflict dedupe.
|
|
108
|
+
* Priority: operator/web > extension > skill > prompt. /reload is single-card.
|
|
109
|
+
*/
|
|
110
|
+
export function mergeSlashCommands(dynamic = []) {
|
|
111
|
+
const byName = new Map();
|
|
112
|
+
const upsert = (item) => {
|
|
113
|
+
const key = normalizeCommandName(item.command);
|
|
114
|
+
if (!key)
|
|
115
|
+
return;
|
|
116
|
+
const existing = byName.get(key);
|
|
117
|
+
if (!existing) {
|
|
118
|
+
byName.set(key, { ...item, command: key });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (SOURCE_PRIORITY[item.source] < SOURCE_PRIORITY[existing.source]) {
|
|
122
|
+
byName.set(key, { ...item, command: key });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
for (const item of SHORTCUTS) {
|
|
126
|
+
upsert({
|
|
127
|
+
command: item.command,
|
|
128
|
+
label: item.label,
|
|
129
|
+
description: item.description,
|
|
130
|
+
source: item.source,
|
|
131
|
+
action: item.action,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
for (const item of WEB_SLASH_COMMANDS) {
|
|
135
|
+
upsert({
|
|
136
|
+
command: item.command,
|
|
137
|
+
label: item.label,
|
|
138
|
+
description: item.description,
|
|
139
|
+
source: item.source,
|
|
140
|
+
action: item.action,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
for (const item of dynamic) {
|
|
144
|
+
const command = normalizeCommandName(item.command);
|
|
145
|
+
if (!command)
|
|
146
|
+
continue;
|
|
147
|
+
upsert({
|
|
148
|
+
command,
|
|
149
|
+
label: item.label || command,
|
|
150
|
+
description: item.description || "",
|
|
151
|
+
source: item.source,
|
|
152
|
+
action: "pi-prompt",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return [...byName.values()].sort((a, b) => {
|
|
156
|
+
const src = SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source];
|
|
157
|
+
if (src !== 0)
|
|
158
|
+
return src;
|
|
159
|
+
return a.command.localeCompare(b.command);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Filter + rank slash candidates by name/description. */
|
|
163
|
+
export function filterSlashCommands(commands, rawQuery) {
|
|
164
|
+
const token = (rawQuery.trim().split(/\s+/, 1)[0] ?? "").toLowerCase();
|
|
165
|
+
// After a space (args), hide the palette.
|
|
166
|
+
if (/\s/.test(rawQuery.trimStart().slice(1)) || rawQuery.includes(" ")) {
|
|
167
|
+
// Only hide when user has typed past the command token.
|
|
168
|
+
const after = rawQuery.trimStart();
|
|
169
|
+
if (after.includes(" "))
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const q = token.startsWith("/") ? token : token ? `/${token}` : "/";
|
|
173
|
+
const needle = q === "/" ? "" : q.slice(1);
|
|
174
|
+
const scored = [];
|
|
175
|
+
for (const item of commands) {
|
|
176
|
+
const name = item.command.toLowerCase();
|
|
177
|
+
const bare = name.startsWith("/") ? name.slice(1) : name;
|
|
178
|
+
const desc = item.description.toLowerCase();
|
|
179
|
+
let score = 100;
|
|
180
|
+
if (!needle) {
|
|
181
|
+
score = 10 + SOURCE_PRIORITY[item.source];
|
|
182
|
+
}
|
|
183
|
+
else if (name === q || bare === needle) {
|
|
184
|
+
score = 0;
|
|
185
|
+
}
|
|
186
|
+
else if (name.startsWith(q) || bare.startsWith(needle)) {
|
|
187
|
+
score = 1;
|
|
188
|
+
}
|
|
189
|
+
else if (bare.includes(needle) || name.includes(needle)) {
|
|
190
|
+
score = 2;
|
|
191
|
+
}
|
|
192
|
+
else if (desc.includes(needle)) {
|
|
193
|
+
score = 3;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
scored.push({ ...item, score });
|
|
199
|
+
}
|
|
200
|
+
return scored.sort((a, b) => {
|
|
201
|
+
if (a.score !== b.score)
|
|
202
|
+
return a.score - b.score;
|
|
203
|
+
const src = SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source];
|
|
204
|
+
if (src !== 0)
|
|
205
|
+
return src;
|
|
206
|
+
return a.command.localeCompare(b.command);
|
|
207
|
+
});
|
|
16
208
|
}
|
|
209
|
+
export const SLASH_SOURCE_LABEL = {
|
|
210
|
+
operator: "Operator",
|
|
211
|
+
web: "Pi Web",
|
|
212
|
+
extension: "Extensions",
|
|
213
|
+
prompt: "Prompts",
|
|
214
|
+
skill: "Skills",
|
|
215
|
+
};
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Human-readable
|
|
2
|
+
* Human-readable tool argument / result projection for Operator Chat (UI-04).
|
|
3
|
+
*
|
|
4
|
+
* Display-only: never mutates raw Pi ToolResult, session JSONL, or model context.
|
|
3
5
|
*/
|
|
4
6
|
const PREVIEW_KEYS = [
|
|
5
7
|
"command",
|
|
8
|
+
"query",
|
|
9
|
+
"operation",
|
|
10
|
+
"tool",
|
|
11
|
+
"server",
|
|
12
|
+
"symbol",
|
|
6
13
|
"path",
|
|
7
14
|
"file_path",
|
|
8
15
|
"filePath",
|
|
9
16
|
"pattern",
|
|
10
|
-
"query",
|
|
11
17
|
"glob",
|
|
12
18
|
"url",
|
|
13
19
|
];
|
|
@@ -34,6 +40,76 @@ function parseArgs(args) {
|
|
|
34
40
|
}
|
|
35
41
|
return asRecord(args);
|
|
36
42
|
}
|
|
43
|
+
function clampOneLine(value, maxLen) {
|
|
44
|
+
const s = value.replace(/\s+/g, " ").trim();
|
|
45
|
+
if (!s)
|
|
46
|
+
return "";
|
|
47
|
+
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
48
|
+
}
|
|
49
|
+
function fieldString(input, key) {
|
|
50
|
+
const value = input[key];
|
|
51
|
+
if (typeof value === "string" && value.trim())
|
|
52
|
+
return value.trim();
|
|
53
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
54
|
+
return String(value);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Tool-specific header adapters (display only). */
|
|
58
|
+
function adapterPreview(toolName, input, maxLen) {
|
|
59
|
+
const name = (toolName ?? "").toLowerCase();
|
|
60
|
+
if (name === "bash" || name === "shell") {
|
|
61
|
+
const command = fieldString(input, "command");
|
|
62
|
+
return command ? clampOneLine(command, maxLen) : undefined;
|
|
63
|
+
}
|
|
64
|
+
if (name === "mcp" || name.startsWith("mcp_")) {
|
|
65
|
+
const server = fieldString(input, "server");
|
|
66
|
+
const tool = fieldString(input, "tool") ?? fieldString(input, "name");
|
|
67
|
+
const query = fieldString(input, "query");
|
|
68
|
+
if (server && tool)
|
|
69
|
+
return clampOneLine(`${server} · ${tool}`, maxLen);
|
|
70
|
+
if (tool)
|
|
71
|
+
return clampOneLine(tool, maxLen);
|
|
72
|
+
if (query)
|
|
73
|
+
return clampOneLine(query, maxLen);
|
|
74
|
+
if (server)
|
|
75
|
+
return clampOneLine(server, maxLen);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (name === "lsp_navigation" || name.includes("lsp")) {
|
|
79
|
+
const parts = [
|
|
80
|
+
fieldString(input, "operation"),
|
|
81
|
+
fieldString(input, "symbol"),
|
|
82
|
+
fieldString(input, "path") ??
|
|
83
|
+
fieldString(input, "file_path") ??
|
|
84
|
+
fieldString(input, "filePath"),
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
const line = fieldString(input, "line") ?? fieldString(input, "position");
|
|
87
|
+
if (parts.length === 0 && !line)
|
|
88
|
+
return undefined;
|
|
89
|
+
const base = parts.join(" · ");
|
|
90
|
+
return clampOneLine(line ? `${base}${base ? ":" : ""}${line}` : base, maxLen);
|
|
91
|
+
}
|
|
92
|
+
if (name === "symbol_search" || name.includes("symbol_search")) {
|
|
93
|
+
const query = fieldString(input, "query") ?? fieldString(input, "symbol");
|
|
94
|
+
return query ? clampOneLine(query, maxLen) : undefined;
|
|
95
|
+
}
|
|
96
|
+
if (name === "read" ||
|
|
97
|
+
name === "read_symbol" ||
|
|
98
|
+
name === "read_enclosing" ||
|
|
99
|
+
name.startsWith("read_")) {
|
|
100
|
+
const path = fieldString(input, "path") ??
|
|
101
|
+
fieldString(input, "file_path") ??
|
|
102
|
+
fieldString(input, "filePath");
|
|
103
|
+
const symbol = fieldString(input, "symbol");
|
|
104
|
+
const line = fieldString(input, "line");
|
|
105
|
+
if (!path && !symbol)
|
|
106
|
+
return undefined;
|
|
107
|
+
const bits = [path, symbol, line].filter(Boolean);
|
|
108
|
+
return clampOneLine(bits.join(" · "), maxLen);
|
|
109
|
+
}
|
|
110
|
+
// Family prefixes (pi-lens / codegraph) — generic field heuristic below.
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
37
113
|
/** One-line summary for a tool-call header; empty when nothing useful. */
|
|
38
114
|
export function getToolPreview(args, options) {
|
|
39
115
|
const maxLen = options?.maxLen ?? DEFAULT_PREVIEW_MAX;
|
|
@@ -47,11 +123,13 @@ export function getToolPreview(args, options) {
|
|
|
47
123
|
}
|
|
48
124
|
return "";
|
|
49
125
|
}
|
|
126
|
+
const adapted = adapterPreview(options?.toolName, input, maxLen);
|
|
127
|
+
if (adapted)
|
|
128
|
+
return adapted;
|
|
50
129
|
for (const key of PREVIEW_KEYS) {
|
|
51
130
|
const value = input[key];
|
|
52
131
|
if (typeof value === "string" && value.trim()) {
|
|
53
|
-
|
|
54
|
-
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
132
|
+
return clampOneLine(value, maxLen);
|
|
55
133
|
}
|
|
56
134
|
}
|
|
57
135
|
const keys = Object.keys(input);
|
|
@@ -63,6 +141,83 @@ export function getToolPreview(args, options) {
|
|
|
63
141
|
return "";
|
|
64
142
|
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
65
143
|
}
|
|
144
|
+
/** Section labels for ToolCard expand body. */
|
|
145
|
+
export function getToolSectionLabels(toolName) {
|
|
146
|
+
const name = (toolName ?? "").toLowerCase();
|
|
147
|
+
if (name === "bash" || name === "shell") {
|
|
148
|
+
return { argsLabel: "命令", resultLabel: "输出" };
|
|
149
|
+
}
|
|
150
|
+
if (name === "mcp" || name.startsWith("mcp_")) {
|
|
151
|
+
return { argsLabel: "参数", resultLabel: "结果" };
|
|
152
|
+
}
|
|
153
|
+
return { argsLabel: "入参", resultLabel: "结果" };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Extract human-readable text from a Pi ToolResult-like value or its
|
|
157
|
+
* already-serialized browser preview string.
|
|
158
|
+
*/
|
|
159
|
+
export function extractToolResultText(result) {
|
|
160
|
+
if (result === undefined || result === null)
|
|
161
|
+
return "";
|
|
162
|
+
if (typeof result === "string") {
|
|
163
|
+
const trimmed = result.trim();
|
|
164
|
+
if (!trimmed)
|
|
165
|
+
return "";
|
|
166
|
+
// Prefer unwrapping standard Pi ToolResult JSON shells.
|
|
167
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
168
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
169
|
+
try {
|
|
170
|
+
return extractToolResultText(JSON.parse(trimmed));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
if (typeof result === "number" || typeof result === "boolean") {
|
|
179
|
+
return String(result);
|
|
180
|
+
}
|
|
181
|
+
const record = asRecord(result);
|
|
182
|
+
if (!record) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.stringify(result, null, 2);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return String(result);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Standard Pi ToolResult: { content: [{ type: "text", text }] }
|
|
191
|
+
const content = record.content;
|
|
192
|
+
if (Array.isArray(content)) {
|
|
193
|
+
const texts = [];
|
|
194
|
+
for (const block of content) {
|
|
195
|
+
const item = asRecord(block);
|
|
196
|
+
if (!item)
|
|
197
|
+
continue;
|
|
198
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
199
|
+
texts.push(item.text);
|
|
200
|
+
}
|
|
201
|
+
else if (typeof item.text === "string") {
|
|
202
|
+
texts.push(item.text);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (texts.length > 0)
|
|
206
|
+
return texts.join("\n");
|
|
207
|
+
}
|
|
208
|
+
if (typeof record.text === "string")
|
|
209
|
+
return record.text;
|
|
210
|
+
if (typeof record.output === "string")
|
|
211
|
+
return record.output;
|
|
212
|
+
if (typeof record.message === "string")
|
|
213
|
+
return record.message;
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(result, null, 2);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return String(result);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
66
221
|
export function formatToolArgsDisplay(args) {
|
|
67
222
|
if (args === undefined || args === null)
|
|
68
223
|
return "";
|
|
@@ -82,7 +237,9 @@ export function truncateToolResult(text, options) {
|
|
|
82
237
|
return { text: `${text.slice(0, maxLen)}…`, truncated: true };
|
|
83
238
|
}
|
|
84
239
|
export function toolCallDurationSeconds(createdAt, finishedAt) {
|
|
85
|
-
if (finishedAt === undefined ||
|
|
240
|
+
if (finishedAt === undefined ||
|
|
241
|
+
!Number.isFinite(createdAt) ||
|
|
242
|
+
!Number.isFinite(finishedAt)) {
|
|
86
243
|
return undefined;
|
|
87
244
|
}
|
|
88
245
|
const secs = Math.round((finishedAt - createdAt) / 1000);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped ownership of in-flight Chat Turn executions.
|
|
3
|
+
*
|
|
4
|
+
* Registry entries must exist before HTTP 202 is returned, and every
|
|
5
|
+
* completion path (success / abort / failure) must clear them in finally so
|
|
6
|
+
* durable active leases cannot outlive the owning process.
|
|
7
|
+
*/
|
|
8
|
+
function keyFor(sessionId, turnId) {
|
|
9
|
+
return `${sessionId}\0${turnId}`;
|
|
10
|
+
}
|
|
11
|
+
export function createChatTurnExecutionRegistry() {
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
const bySession = new Map();
|
|
14
|
+
return {
|
|
15
|
+
register(input) {
|
|
16
|
+
const existingSession = bySession.get(input.sessionId);
|
|
17
|
+
if (existingSession && existingSession !== input.turnId) {
|
|
18
|
+
throw new Error(`CHAT_TURN_EXECUTION_ACTIVE: ${input.sessionId} already owns ${existingSession}`);
|
|
19
|
+
}
|
|
20
|
+
const key = keyFor(input.sessionId, input.turnId);
|
|
21
|
+
const prior = entries.get(key);
|
|
22
|
+
if (prior)
|
|
23
|
+
return prior;
|
|
24
|
+
let resolveCompletion;
|
|
25
|
+
const completion = new Promise((resolve) => {
|
|
26
|
+
resolveCompletion = resolve;
|
|
27
|
+
});
|
|
28
|
+
// Prevent unhandled rejection if nobody awaits.
|
|
29
|
+
void completion.catch(() => undefined);
|
|
30
|
+
const entry = {
|
|
31
|
+
sessionId: input.sessionId,
|
|
32
|
+
turnId: input.turnId,
|
|
33
|
+
clientRequestId: input.clientRequestId,
|
|
34
|
+
controller: input.controller ?? new AbortController(),
|
|
35
|
+
completion,
|
|
36
|
+
resolveCompletion,
|
|
37
|
+
};
|
|
38
|
+
entries.set(key, entry);
|
|
39
|
+
bySession.set(input.sessionId, input.turnId);
|
|
40
|
+
return entry;
|
|
41
|
+
},
|
|
42
|
+
get(sessionId, turnId) {
|
|
43
|
+
return entries.get(keyFor(sessionId, turnId));
|
|
44
|
+
},
|
|
45
|
+
getBySession(sessionId) {
|
|
46
|
+
const turnId = bySession.get(sessionId);
|
|
47
|
+
if (!turnId)
|
|
48
|
+
return undefined;
|
|
49
|
+
return entries.get(keyFor(sessionId, turnId));
|
|
50
|
+
},
|
|
51
|
+
clear(sessionId, turnId) {
|
|
52
|
+
const key = keyFor(sessionId, turnId);
|
|
53
|
+
entries.delete(key);
|
|
54
|
+
if (bySession.get(sessionId) === turnId)
|
|
55
|
+
bySession.delete(sessionId);
|
|
56
|
+
},
|
|
57
|
+
async waitForCompletion(sessionId, turnId, timeoutMs) {
|
|
58
|
+
const entry = entries.get(keyFor(sessionId, turnId));
|
|
59
|
+
if (!entry)
|
|
60
|
+
return { status: "missing" };
|
|
61
|
+
let timer;
|
|
62
|
+
try {
|
|
63
|
+
const outcome = await Promise.race([
|
|
64
|
+
entry.completion.then((value) => ({ kind: "done", value })),
|
|
65
|
+
new Promise((resolve) => {
|
|
66
|
+
timer = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs);
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
if (outcome.kind === "timeout")
|
|
70
|
+
return { status: "timeout" };
|
|
71
|
+
return { status: "completed", outcome: outcome.value };
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (timer)
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
size() {
|
|
79
|
+
return entries.size;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -22,6 +22,19 @@ import path from "node:path";
|
|
|
22
22
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
23
23
|
import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "./app-data.js";
|
|
24
24
|
import { hashDagBytes, } from "./dag-confirmation.js";
|
|
25
|
+
/**
|
|
26
|
+
* Structured verification command patterns for the G2 autonomous-execution
|
|
27
|
+
* gate. A DAG is only eligible for autonomous execution when it carries at
|
|
28
|
+
* least one shell verification node whose id or command is recognized as a
|
|
29
|
+
* real test/verification suite (not just an arbitrary shell command).
|
|
30
|
+
*
|
|
31
|
+
* Covers the Console workflow kinds: standard/frontend (npm run test/lint/
|
|
32
|
+
* typecheck, vitest, check-repo.sh), backend-test (pytest) and frontend-test
|
|
33
|
+
* (playwright). Ids ending in -verify / -verification / -validate / -gate are
|
|
34
|
+
* treated as verification shells by the caller; commands broaden the match to
|
|
35
|
+
* framework invocations that don't embed the word "verify".
|
|
36
|
+
*/
|
|
37
|
+
export const STRUCTURED_VERIFICATION_COMMAND_RE = /\b(vitest|jest|playwright|cypress|pytest|py-test|go\s+test|cargo\s+test|npm\s+run\s+(lint|typecheck|test|verify)|npm\s+(test|run\s+test)|check-repo\.sh|loop-agent-standard-verify)\b/i;
|
|
25
38
|
export const DEFAULT_EXECUTION_RECEIPT_TTL_MS = 30 * 60 * 1000;
|
|
26
39
|
function normalizePathEntry(entry) {
|
|
27
40
|
return entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -145,7 +158,7 @@ export function deriveEligibilityFactsFromDagSpec(input) {
|
|
|
145
158
|
if (/verify|verification/i.test(task.id ?? ""))
|
|
146
159
|
return true;
|
|
147
160
|
const commands = stringList(task.shell.commands);
|
|
148
|
-
return commands.some((command) =>
|
|
161
|
+
return commands.some((command) => STRUCTURED_VERIFICATION_COMMAND_RE.test(command));
|
|
149
162
|
});
|
|
150
163
|
return {
|
|
151
164
|
writersWriteSets: writers,
|
|
@@ -54,7 +54,7 @@ async function probeInspect(consoleBaseUrl, fetchImpl) {
|
|
|
54
54
|
source: "static-assets",
|
|
55
55
|
routeCapabilities,
|
|
56
56
|
message: ready
|
|
57
|
-
? "Inspect 静态资源可用;启动 agent-worker console
|
|
57
|
+
? "Inspect 静态资源可用;启动 agent-worker console 以提供统一入口"
|
|
58
58
|
: "Inspect 静态资源缺失;请运行构建或检查发布包",
|
|
59
59
|
};
|
|
60
60
|
}
|
|
@@ -2,6 +2,7 @@ export { createConsoleServer, defaultConsoleStaticDir } from "./server.js";
|
|
|
2
2
|
export { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
|
|
3
3
|
export { runConsoleDoctor, formatConsoleDoctorHuman } from "./doctor.js";
|
|
4
4
|
export { isLoopbackHost } from "./loopback.js";
|
|
5
|
+
export { shouldOpenConsoleBrowser, normalizeConsoleBrowserUrl, openSystemBrowser, maybeOpenConsoleBrowser, } from "./open-browser.js";
|
|
5
6
|
export { repoFingerprintV1, normalizeWorktreeRealpath, } from "./repo-fingerprint.js";
|
|
6
7
|
export { buildObserveDeepLink } from "./observe-link.js";
|
|
7
8
|
export { classifyObserveHealth, probeAndClassifyObserve, capabilityForObserveTarget, OBSERVE_START_COMMAND, DEFAULT_OBSERVE_BASE_URL, } from "./observe-health-match.js";
|