@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/worker/console/static/assets/index-Bpa2qrc-.js +29 -0
  3. package/dist/worker/console/static/assets/index-BqfFDdnG.css +1 -0
  4. package/dist/worker/console/static/index.html +2 -2
  5. package/dist/worker/console/static-src/app/console-types.js +140 -0
  6. package/dist/worker/console/static-src/app/useConsoleShell.js +101 -0
  7. package/dist/worker/console/static-src/app/useOperatorActions.js +304 -0
  8. package/dist/worker/console/static-src/app/usePrdImport.js +171 -0
  9. package/dist/worker/console/static-src/app/useRecoveryActions.js +257 -0
  10. package/dist/worker/console/static-src/app/useRecoveryConsole.js +334 -0
  11. package/dist/worker/console/static-src/app/useTaskWizard.js +229 -0
  12. package/dist/worker/console/static-src/chat-view-types.js +2 -0
  13. package/dist/worker/console/static-src/night/night-types.js +24 -0
  14. package/dist/worker/console/static-src/night/useNightBoard.js +125 -0
  15. package/dist/worker/console/static-src/night/useNightWizard.js +171 -0
  16. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +253 -0
  17. package/dist/worker/console/static-src/operator-chat/format.js +71 -0
  18. package/dist/worker/console/static-src/operator-chat/refs.js +47 -0
  19. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +77 -0
  20. package/dist/worker/console/static-src/operator-chat/types.js +1 -0
  21. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +320 -0
  22. package/dist/worker/console/static-src/operator-chat/useChatStream.js +209 -0
  23. package/dist/worker/console/static-src/operator-chat/useChatThread.js +218 -0
  24. package/dist/worker/console/static-src/operator-chat/useComposer.js +125 -0
  25. package/dist/worker/console/static-src/operator-chat/useInterview.js +108 -0
  26. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +123 -0
  27. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +207 -0
  28. package/docs/templates/agent-worker-production-readiness-checklist.md +26 -24
  29. package/package.json +5 -4
  30. package/dist/worker/console/static/assets/index-D9gnJn_l.js +0 -29
  31. 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
- - [ ] controller packageVersion + fingerprint 已记录
9
- - [ ] npm link / workspace bin 冒充正式 controller
10
- - [ ] protocol/capabilities preflight 通过
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
- - [ ] Running 投影不依赖 terminal ledger
15
- - [ ] Task-scoped events 含 featureId
16
- - [ ] dual Feature 同名 Task 隔离
17
- - [ ] ownership warning 按根因聚合(≤1 finding/根因)
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
- - [ ] safe run-error → Failed → `task retry` 无需 mark-failed
22
- - [ ] Blocked 保留 contract/human 语义
23
- - [ ] resume / retry / reconcile / revise / decide 决策一致
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
- - [ ] `{featureId, taskId}` 单 active writer
28
- - [ ] begin/finalize crash injection 有 doctor finding
29
- - [ ] 不盲目偷取 live DAG lease
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
- - [ ] Task Done ≠ AC covered
34
- - [ ] `feature verify-final` 唯一 writer
35
- - [ ] verify-final → delivery → closeout 可收口
36
- - [ ] dirty HEAD / tamper fail-closed
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
- - [ ] C0–C11 已执行或明确 blocked-external
41
- - [ ] burn-in 窗口达标(7 日或 30 Attempt
42
- - [ ] SLO 分子/分母已记录
43
- - [ ] promote + rollback drill 完成
44
- - [ ] scorecard: READY 或合法 CONDITIONALLY_READY
45
- - [ ] open P0 = 0
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.0",
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:build": "vite build --config src/worker/console/vite.config.ts",
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": "tsc --noEmit",
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",