@tea-agent/loop-agent 0.32.0 → 0.32.1
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/CHANGELOG.md +14 -0
- package/dist/worker/console/static/assets/index-Bpa2qrc-.js +29 -0
- package/dist/worker/console/static/assets/index-BqfFDdnG.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +140 -0
- package/dist/worker/console/static-src/app/useConsoleShell.js +101 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +304 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +171 -0
- package/dist/worker/console/static-src/app/useRecoveryActions.js +257 -0
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +334 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +229 -0
- package/dist/worker/console/static-src/chat-view-types.js +2 -0
- package/dist/worker/console/static-src/night/night-types.js +24 -0
- package/dist/worker/console/static-src/night/useNightBoard.js +125 -0
- package/dist/worker/console/static-src/night/useNightWizard.js +171 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +253 -0
- package/dist/worker/console/static-src/operator-chat/format.js +71 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +47 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +77 -0
- package/dist/worker/console/static-src/operator-chat/types.js +1 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +320 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +209 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +218 -0
- package/dist/worker/console/static-src/operator-chat/useComposer.js +125 -0
- package/dist/worker/console/static-src/operator-chat/useInterview.js +108 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +123 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +207 -0
- package/docs/templates/agent-worker-production-readiness-checklist.md +26 -24
- package/package.json +5 -4
- package/dist/worker/console/static/assets/index-D9gnJn_l.js +0 -29
- package/dist/worker/console/static/assets/index-rajoXwkM.css +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { formatModelRef, mergeSessionRuntime, runtimeSelectionFromRecord, sessionRuntimeMatches, } from "../../chat/runtime-selection.js";
|
|
3
|
+
import { confirmationToken } from "./format.js";
|
|
4
|
+
import { buildToolCatalog } from "./tools-catalog.js";
|
|
5
|
+
/** Capabilities catalog, model / thinking-level switcher, runtime-context
|
|
6
|
+
* popover and the outside-click dismissal for both transient menus. */
|
|
7
|
+
export function useRuntimeControls(params) {
|
|
8
|
+
const { origin, session, setSession, refs, setError } = params;
|
|
9
|
+
const [capabilities, setCapabilities] = useState(null);
|
|
10
|
+
const [models, setModels] = useState([]);
|
|
11
|
+
const [thinkingLevels, setThinkingLevels] = useState([]);
|
|
12
|
+
const [runtimeContext, setRuntimeContext] = useState(null);
|
|
13
|
+
const [toolsOpen, setToolsOpen] = useState(false);
|
|
14
|
+
const [runtimeContextOpen, setRuntimeContextOpen] = useState(false);
|
|
15
|
+
const toolsMenuRef = useRef(null);
|
|
16
|
+
const runtimeContextMenuRef = useRef(null);
|
|
17
|
+
const loadCapabilities = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/capabilities`);
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
return;
|
|
22
|
+
const body = (await res.json());
|
|
23
|
+
setCapabilities(body);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// capabilities optional; Chat still works without them
|
|
27
|
+
}
|
|
28
|
+
}, [origin]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
void loadCapabilities();
|
|
31
|
+
}, [loadCapabilities]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!session) {
|
|
34
|
+
setModels([]);
|
|
35
|
+
setRuntimeContext(null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const sessionId = session.sessionId;
|
|
39
|
+
const base = `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
40
|
+
void fetch(`${base}/models`, { credentials: "include" })
|
|
41
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
42
|
+
.then((body) => {
|
|
43
|
+
// Guard against a stale response landing after the user switched/
|
|
44
|
+
// created/forked a session: only adopt models for the session we
|
|
45
|
+
// actually requested them for.
|
|
46
|
+
if (!body || refs.sessionRef.current?.sessionId !== sessionId)
|
|
47
|
+
return;
|
|
48
|
+
setModels(body.models ?? []);
|
|
49
|
+
setThinkingLevels(body.thinkingLevels ?? []);
|
|
50
|
+
});
|
|
51
|
+
void fetch(`${base}/runtime-context`, { credentials: "include" })
|
|
52
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
53
|
+
.then((body) => {
|
|
54
|
+
// Stale-response guard: a slow runtime-context reply for session A
|
|
55
|
+
// must not overwrite the context/session state of the now-current
|
|
56
|
+
// session B.
|
|
57
|
+
if (!body?.context || refs.sessionRef.current?.sessionId !== sessionId)
|
|
58
|
+
return;
|
|
59
|
+
setRuntimeContext(body.context);
|
|
60
|
+
const ctx = body.context;
|
|
61
|
+
const patch = {
|
|
62
|
+
model: ctx.model,
|
|
63
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
64
|
+
};
|
|
65
|
+
setSession((current) => {
|
|
66
|
+
if (!current || current.sessionId !== sessionId)
|
|
67
|
+
return current;
|
|
68
|
+
if (sessionRuntimeMatches(current, patch))
|
|
69
|
+
return current;
|
|
70
|
+
const merged = mergeSessionRuntime(current, patch);
|
|
71
|
+
refs.sessionRef.current = merged;
|
|
72
|
+
return merged;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}, [origin, session?.sessionId, refs, setSession]);
|
|
76
|
+
const switchRuntime = useCallback(async (body) => {
|
|
77
|
+
if (!session)
|
|
78
|
+
return;
|
|
79
|
+
// Capture the target session so a slow model/thinking response cannot
|
|
80
|
+
// write its result into a different (newly created/forked/switched)
|
|
81
|
+
// session the user has since moved to.
|
|
82
|
+
const targetSessionId = session.sessionId;
|
|
83
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(session.sessionId)}/model`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
credentials: "include",
|
|
86
|
+
headers: {
|
|
87
|
+
"content-type": "application/json",
|
|
88
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(body),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
// Only surface the failure for the session that still owns it.
|
|
94
|
+
if (refs.sessionRef.current?.sessionId === targetSessionId)
|
|
95
|
+
setError("模型或思考等级切换失败");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const payload = (await res.json().catch(() => ({})));
|
|
99
|
+
if (!payload.session)
|
|
100
|
+
return;
|
|
101
|
+
// Drop the result entirely if the active session changed mid-flight.
|
|
102
|
+
if (refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
103
|
+
return;
|
|
104
|
+
setSession((current) => {
|
|
105
|
+
if (!current || current.sessionId !== targetSessionId)
|
|
106
|
+
return current;
|
|
107
|
+
const patch = runtimeSelectionFromRecord(payload.session);
|
|
108
|
+
if (sessionRuntimeMatches(current, patch))
|
|
109
|
+
return current;
|
|
110
|
+
const merged = mergeSessionRuntime(current, patch);
|
|
111
|
+
refs.sessionRef.current = merged;
|
|
112
|
+
return merged;
|
|
113
|
+
});
|
|
114
|
+
setRuntimeContext((current) => current
|
|
115
|
+
? {
|
|
116
|
+
...current,
|
|
117
|
+
...(payload.session.modelProvider && payload.session.modelId
|
|
118
|
+
? {
|
|
119
|
+
model: {
|
|
120
|
+
provider: payload.session.modelProvider,
|
|
121
|
+
modelId: payload.session.modelId,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
...(payload.session.thinkingLevel
|
|
126
|
+
? { thinkingLevel: payload.session.thinkingLevel }
|
|
127
|
+
: body.thinkingLevel
|
|
128
|
+
? { thinkingLevel: body.thinkingLevel }
|
|
129
|
+
: {}),
|
|
130
|
+
}
|
|
131
|
+
: current);
|
|
132
|
+
}, [origin, session, refs, setSession, setError]);
|
|
133
|
+
// Close tools catalog on outside click / Escape.
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!toolsOpen)
|
|
136
|
+
return;
|
|
137
|
+
const onPointer = (e) => {
|
|
138
|
+
const root = toolsMenuRef.current;
|
|
139
|
+
if (!root)
|
|
140
|
+
return;
|
|
141
|
+
if (e.target instanceof Node && !root.contains(e.target)) {
|
|
142
|
+
setToolsOpen(false);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const onKey = (e) => {
|
|
146
|
+
if (e.key === "Escape")
|
|
147
|
+
setToolsOpen(false);
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("mousedown", onPointer);
|
|
150
|
+
document.addEventListener("keydown", onKey);
|
|
151
|
+
return () => {
|
|
152
|
+
document.removeEventListener("mousedown", onPointer);
|
|
153
|
+
document.removeEventListener("keydown", onKey);
|
|
154
|
+
};
|
|
155
|
+
}, [toolsOpen]);
|
|
156
|
+
// Close runtime-context popover on outside click / Escape.
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!runtimeContextOpen)
|
|
159
|
+
return;
|
|
160
|
+
const onPointer = (e) => {
|
|
161
|
+
const root = runtimeContextMenuRef.current;
|
|
162
|
+
if (!root)
|
|
163
|
+
return;
|
|
164
|
+
if (e.target instanceof Node && !root.contains(e.target)) {
|
|
165
|
+
setRuntimeContextOpen(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const onKey = (e) => {
|
|
169
|
+
if (e.key === "Escape")
|
|
170
|
+
setRuntimeContextOpen(false);
|
|
171
|
+
};
|
|
172
|
+
document.addEventListener("mousedown", onPointer);
|
|
173
|
+
document.addEventListener("keydown", onKey);
|
|
174
|
+
return () => {
|
|
175
|
+
document.removeEventListener("mousedown", onPointer);
|
|
176
|
+
document.removeEventListener("keydown", onKey);
|
|
177
|
+
};
|
|
178
|
+
}, [runtimeContextOpen]);
|
|
179
|
+
const toolCatalog = useMemo(() => buildToolCatalog(capabilities), [
|
|
180
|
+
capabilities,
|
|
181
|
+
]);
|
|
182
|
+
const activeModel = session?.model ?? runtimeContext?.model;
|
|
183
|
+
const activeModelRef = activeModel
|
|
184
|
+
? formatModelRef(activeModel.provider, activeModel.modelId)
|
|
185
|
+
: "";
|
|
186
|
+
const activeThinkingLevel = session?.thinkingLevel ?? runtimeContext?.thinkingLevel ?? "";
|
|
187
|
+
const activeModelListed = !activeModelRef ||
|
|
188
|
+
models.some((model) => formatModelRef(model.provider, model.id) === activeModelRef);
|
|
189
|
+
return {
|
|
190
|
+
capabilities,
|
|
191
|
+
models,
|
|
192
|
+
thinkingLevels,
|
|
193
|
+
runtimeContext,
|
|
194
|
+
toolsOpen,
|
|
195
|
+
setToolsOpen,
|
|
196
|
+
runtimeContextOpen,
|
|
197
|
+
setRuntimeContextOpen,
|
|
198
|
+
toolsMenuRef,
|
|
199
|
+
runtimeContextMenuRef,
|
|
200
|
+
switchRuntime,
|
|
201
|
+
toolCatalog,
|
|
202
|
+
activeModel,
|
|
203
|
+
activeModelRef,
|
|
204
|
+
activeThinkingLevel,
|
|
205
|
+
activeModelListed,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -2,44 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
用于 Phase 8 campaign / 发布门禁。勾选前必须有新鲜命令输出或 report ref。
|
|
4
4
|
|
|
5
|
+
正式 pin(2026-08-09):`@tea-agent/loop-agent@0.32.0`(tag `v0.32.0` / `b4d9e500`)。scorecard 在 burn-in / live 门禁完成前保持 **NOT_READY**。证据:`docs/reports/feature/2026-08-09-agent-worker-production-readiness-v1.md`。
|
|
6
|
+
|
|
5
7
|
## 包络与身份
|
|
6
8
|
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
9
|
+
- [x] 支持包络已冻结(单机/单用户/单仓)
|
|
10
|
+
- [x] controller packageVersion + fingerprint 已记录(`0.32.0`;npm shasum `8932749b1583523043a9889c84f9d316e57e451d`;canary fingerprint `sha256:b08404f5…`)
|
|
11
|
+
- [x] published canary / 隔离 install **无** npm link / workspace bin(live burn-in 仍须每日复核)
|
|
12
|
+
- [x] protocol/capabilities preflight(unit)通过;live preflight 仍待
|
|
11
13
|
|
|
12
14
|
## Ownership / Attempt
|
|
13
15
|
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
16
|
+
- [x] Running 投影不依赖 terminal ledger(unit / Phase 1)
|
|
17
|
+
- [x] Task-scoped events 含 featureId(unit)
|
|
18
|
+
- [x] dual Feature 同名 Task 隔离(unit C2)
|
|
19
|
+
- [x] ownership warning 按根因聚合(≤1 finding/根因)(unit)
|
|
18
20
|
|
|
19
21
|
## Lifecycle / Recovery
|
|
20
22
|
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
23
|
+
- [x] safe run-error → Failed → `task retry` 无需 mark-failed(unit)
|
|
24
|
+
- [x] Blocked 保留 contract/human 语义(unit)
|
|
25
|
+
- [x] resume / retry / reconcile / revise / decide 决策一致(unit)
|
|
24
26
|
|
|
25
27
|
## Lease
|
|
26
28
|
|
|
27
|
-
- [
|
|
28
|
-
- [ ] begin/finalize crash injection 有 doctor finding
|
|
29
|
-
- [
|
|
29
|
+
- [x] `{featureId, taskId}` 单 active writer(unit C3)
|
|
30
|
+
- [ ] begin/finalize crash injection 有 doctor finding(进程级仍缺)
|
|
31
|
+
- [x] 不盲目偷取 live DAG lease(unit reclaim)
|
|
30
32
|
|
|
31
33
|
## Acceptance / Delivery
|
|
32
34
|
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [ ] verify-final → delivery → closeout 可收口
|
|
36
|
-
- [
|
|
35
|
+
- [x] Task Done ≠ AC covered(unit)
|
|
36
|
+
- [x] `feature verify-final` 唯一 writer(unit)
|
|
37
|
+
- [ ] verify-final → delivery → closeout **live** 可收口
|
|
38
|
+
- [x] dirty HEAD / tamper fail-closed(unit)
|
|
37
39
|
|
|
38
40
|
## Campaign / SLO
|
|
39
41
|
|
|
40
|
-
- [
|
|
41
|
-
- [ ] burn-in 窗口达标(7 日或 30 Attempt
|
|
42
|
-
- [ ] SLO
|
|
43
|
-
- [
|
|
44
|
-
- [ ] scorecard: READY 或合法 CONDITIONALLY_READY
|
|
45
|
-
- [
|
|
42
|
+
- [x] C0–C11 已执行或明确 blocked-external(partial:unit + published canary;live Pi blocked)
|
|
43
|
+
- [ ] burn-in 窗口达标(7 日或 30 Attempt)— **OPEN day 0**
|
|
44
|
+
- [ ] SLO 分子/分母已记录(正式样本不足)
|
|
45
|
+
- [x] promote + rollback drill 完成(install-level vs `0.31.1`)
|
|
46
|
+
- [ ] scorecard: READY 或合法 CONDITIONALLY_READY — 当前 **`NOT_READY`**
|
|
47
|
+
- [x] open P0 = 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tea-agent/loop-agent",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"loop-agent": "bin/loop-agent.js",
|
|
@@ -46,11 +46,12 @@
|
|
|
46
46
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
47
47
|
"brand:sync": "node scripts/sync-brand-assets.mjs",
|
|
48
48
|
"build": "npm run brand:sync && npm run clean && tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');const p='dist/worker/observe/static';fs.mkdirSync(p,{recursive:true});fs.cpSync('src/worker/observe/static',p,{recursive:true});\" && npm run console:build",
|
|
49
|
-
"console:
|
|
49
|
+
"console:typecheck": "tsc -p src/worker/console/tsconfig.json",
|
|
50
|
+
"console:build": "npm run console:typecheck && vite build --config src/worker/console/vite.config.ts",
|
|
50
51
|
"prepack": "npm run build",
|
|
51
52
|
"prepublishOnly": "node scripts/check-npm-publish-policy.mjs && npm run typecheck && npm test && npm run build",
|
|
52
|
-
"lint": "
|
|
53
|
-
"typecheck": "tsc --noEmit",
|
|
53
|
+
"lint": "npm run typecheck",
|
|
54
|
+
"typecheck": "tsc --noEmit && npm run console:typecheck",
|
|
54
55
|
"test": "node scripts/run-tests.mjs",
|
|
55
56
|
"test:host": "vitest run test/init-upgrade.test.ts --maxWorkers=1 -t \"records and reuses|controller-authorized semantic merge|authority-sensitive\"",
|
|
56
57
|
"test:fast": "vitest run --config vitest.fast.config.ts",
|