@tea-agent/loop-agent 0.35.1 → 0.35.3
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 +2 -0
- package/CHANGELOG.md +51 -0
- package/README.md +1 -1
- package/bin/loop-agent.js +37 -1
- package/dist/build-stamp.json +6 -0
- package/dist/cli/program.js +2 -2
- package/dist/executors/dag-pi-executor.js +44 -0
- package/dist/shared/package-metadata.js +42 -0
- package/dist/worker/console/chat/assistant-content.js +121 -0
- package/dist/worker/console/chat/pi-runtime.js +17 -32
- package/dist/worker/console/chat/routes.js +9 -4
- package/dist/worker/console/chat/session-store.js +3 -0
- package/dist/worker/console/chat/shortcuts.js +9 -7
- package/dist/worker/console/chat/turn-process.js +70 -15
- package/dist/worker/console/chat/workspace-landing.js +1 -1
- package/dist/worker/console/static/assets/index-DRqZiQ7J.css +1 -0
- package/dist/worker/console/static/assets/index-DuVLjCIT.js +57 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +24 -3
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +14 -6
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +12 -6
- package/dist/worker/console/static-src/operator-chat/useComposer.js +15 -1
- package/dist/worker/loop-agent/loop-agent-client.js +17 -3
- package/dist/worker/observability/read-model.js +20 -0
- package/dist/worker/observe/spec-evidence.js +3 -8
- package/dist/worker/observe/static/views/dag-inspector.js +6 -71
- package/dist/worker/preflight.js +2 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +33 -23
- package/dist/workflows/dag/contract-output-registry.js +14 -0
- package/dist/workflows/dag/contract-validator-registrations.js +8 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +9 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +233 -39
- package/dist/workflows/dag/frontend-prewrite-gate.js +364 -61
- package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
- package/dist/workflows/dag/frontend-recovery-run.js +539 -0
- package/dist/workflows/dag/frontend-repair.js +219 -18
- package/dist/workflows/dag/frontend-verification-trace.js +47 -32
- package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
- package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
- package/dist/workflows/dag/init-hybrid.js +49 -24
- package/dist/workflows/dag/node-execution.js +89 -0
- package/dist/workflows/dag/recovery-recommendation.js +58 -0
- package/dist/workflows/dag/runner.js +245 -11
- package/dist/workflows/dag/scheduler.js +257 -3
- package/dist/workflows/dag/types.js +130 -2
- package/harness.json +1 -1
- package/package.json +4 -3
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +0 -1
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +0 -56
package/AGENTS.md
CHANGED
|
@@ -94,6 +94,8 @@ npm run build
|
|
|
94
94
|
node bin/loop-agent.js --help
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
- 改 `src/**` 后必须先 `npm run build` 再用 `bin/loop-agent.js`:bin 内置 stale-dist 守卫(src 比 dist 新即硬失败,`--ignore-stale-dist` 逃生),`--version` 会显示构建 git SHA,发布/自测前用它确认跑的是目标改动。
|
|
98
|
+
|
|
97
99
|
文档站变更:`npm run docs:build`。init / architecture / skill entry / pack 定向验证见 verification-matrix。
|
|
98
100
|
|
|
99
101
|
## 交接
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.35.3] - 2026-08-15
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- chat as default landing + analyze-skills-first new-task flow
|
|
10
|
+
- guide plan verification targets to writeSet-internal files, command-level checks to static
|
|
11
|
+
- console recovery display — surface frontend recovery lineage + outcome
|
|
12
|
+
- frontend-implementation DAG phase 6 — recovery root manifest + statistics dedup
|
|
13
|
+
- frontend-implementation DAG phase 5 — writer partial-write rollback + child recovery
|
|
14
|
+
- frontend-implementation DAG phase 4 — writer transient rollback base
|
|
15
|
+
- frontend-implementation DAG phase 3c — scheduler activation gate + recovery trigger
|
|
16
|
+
- frontend-implementation DAG phase 3b — staged child-creation kernel
|
|
17
|
+
- frontend-implementation DAG phase 3a — recovery state/result types + terminal recovery intent
|
|
18
|
+
- producing-node contract self-check and run robustness hardening
|
|
19
|
+
- frontend-implementation DAG phase 2 — unified writer helper + structured repair assessment
|
|
20
|
+
- slim frontend-implementation DAG phase 1 — prewrite merge + writer admission gate
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- 规范证据面板不再单列 openspec,统一为「规范已读取/规范检索」
|
|
25
|
+
- green full test suite — python3 fallback, build-stamp version normalization, DEP0205 suppression
|
|
26
|
+
- update candidate-continuation fixtures for failureCode (rebase semantic fix)
|
|
27
|
+
|
|
28
|
+
## [0.35.2] - 2026-08-15
|
|
29
|
+
|
|
30
|
+
### 重点更新
|
|
31
|
+
|
|
32
|
+
- Operator Chat 处理详情对齐 pi-web:回合尾部的思考内容与工具调用摘要切分为可折叠「处理详情」,流式中思考可见,回合结束后自动折叠,最终回答单独成气泡
|
|
33
|
+
|
|
34
|
+
### 新增
|
|
35
|
+
|
|
36
|
+
- 对话时间线新增内联可折叠「处理详情」区块,集中展示模型思考与工具调用摘要,回合结束后自动折叠
|
|
37
|
+
- turn-process 保留 SDK thinking blocks,流式传输与持久化均携带思考内容
|
|
38
|
+
|
|
39
|
+
### 改进
|
|
40
|
+
|
|
41
|
+
- 命令面板 shortcut 优化:Operator 组单行展示,/task、/contract、/dag 分别跳转到任务与恢复页面
|
|
42
|
+
|
|
43
|
+
### 修复
|
|
44
|
+
|
|
45
|
+
- 修复「处理详情」思考折叠块背景引用未定义样式变量 --panel 的问题,改为确定性透明背景,消除 Console 样式变量完整性门禁对发布的拦截(视觉无变化)
|
|
46
|
+
- 移除自指的 /help 命令项:输入 / 已弹出命令面板,不再单独提供重复的帮助入口
|
|
47
|
+
|
|
5
48
|
## [0.35.1] - 2026-08-15
|
|
6
49
|
|
|
7
50
|
### 重点更新
|
|
@@ -31,6 +74,14 @@
|
|
|
31
74
|
- 修复文字录入「请参照这三个prd路径XXX」只物化一句话、引用文件从未导入的问题
|
|
32
75
|
- 修复 semantic intake attempt marker 一次性失败后永不重试的问题:新的 PRD 导入会重置 marker,失败可随新证据重试
|
|
33
76
|
|
|
77
|
+
## [0.35.1-beta.4] - 2026-08-15
|
|
78
|
+
|
|
79
|
+
> 观察面板规范证据不再单列 openspec:读取与检索统一为「规范已读取 / 规范检索」,空态文案同步中性化(beta,发布到 `beta` dist-tag)。
|
|
80
|
+
|
|
81
|
+
## [0.35.1-beta.3] - 2026-08-14
|
|
82
|
+
|
|
83
|
+
> frontend-implementation DAG 优化 v9 全六阶段 + Console 展示层(beta,发布到 `beta` dist-tag)。
|
|
84
|
+
|
|
34
85
|
## [0.35.0] - 2026-08-13
|
|
35
86
|
|
|
36
87
|
### 重点更新
|
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ agent-worker console doctor --repo .
|
|
|
110
110
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
访问 Console <http://127.0.0.1:8790/>;**Operator Chat Official** 是 Console
|
|
113
|
+
访问 Console <http://127.0.0.1:8790/>;**Operator Chat Official** 是 Console 内的主引导交互,默认打开「对话」工作区。构建时设置 `VITE_OPERATOR_CHAT_DEFAULT_LANDING=tasks` 可改回 Tasks;URL hash(`#/tasks`)与 localStorage 仍优先于默认值。顶部「检视」以及 `/inspect/` 提供只读 Dashboard、DAG、时间线与 artifacts;`loop-agent …` / `agent-worker …` CLI direct path 仍是受治理 fallback。当前状态为 **code ready, dogfood pending/unmet**,不宣称已替代 openCode 或其他外部主会话。
|
|
114
114
|
|
|
115
115
|
## 核心边界
|
|
116
116
|
|
package/bin/loop-agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
|
|
@@ -13,6 +13,42 @@ if (!existsSync(cliEntry)) {
|
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Stale-dist guard: when running from a source checkout (src/ present), refuse
|
|
18
|
+
* to silently execute a dist/ that predates the latest source edit. The
|
|
19
|
+
* classic failure mode is editing src/** and running the bin without
|
|
20
|
+
* `npm run build`, which executes the previous build. Suppress with
|
|
21
|
+
* --ignore-stale-dist when intentionally running a stale build.
|
|
22
|
+
*/
|
|
23
|
+
const srcDir = join(packageRoot, "src");
|
|
24
|
+
const ignoreStale = process.argv.includes("--ignore-stale-dist");
|
|
25
|
+
if (ignoreStale) {
|
|
26
|
+
process.argv = process.argv.filter((arg) => arg !== "--ignore-stale-dist");
|
|
27
|
+
}
|
|
28
|
+
if (!ignoreStale && existsSync(srcDir)) {
|
|
29
|
+
const distCliMtime = statSync(cliEntry).mtimeMs;
|
|
30
|
+
let newestSrcMtime = 0;
|
|
31
|
+
const walk = (dir) => {
|
|
32
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const full = join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
walk(full);
|
|
36
|
+
} else {
|
|
37
|
+
const mtime = statSync(full).mtimeMs;
|
|
38
|
+
if (mtime > newestSrcMtime) newestSrcMtime = mtime;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
walk(srcDir);
|
|
43
|
+
if (newestSrcMtime > distCliMtime) {
|
|
44
|
+
console.error(
|
|
45
|
+
`loop-agent: dist/ is stale relative to src/ (a source file is newer than ${cliEntry}).\n` +
|
|
46
|
+
` Run \`npm run build\` before using the bin, or pass --ignore-stale-dist to run the stale build intentionally.`,
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
try {
|
|
17
53
|
await import(pathToFileURL(cliEntry).href);
|
|
18
54
|
} catch (error) {
|
package/dist/cli/program.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import {
|
|
4
|
+
import { readCliVersionLabel } from "../shared/package-metadata.js";
|
|
5
5
|
import { resolveAdapter } from "../adapters/index.js";
|
|
6
6
|
import { runCoverageAudit } from "../commands/coverage-audit.js";
|
|
7
7
|
import { runCoverageReport } from "../commands/coverage-report.js";
|
|
@@ -547,7 +547,7 @@ export function buildLoopAgentProgram(options) {
|
|
|
547
547
|
program
|
|
548
548
|
.name("loop-agent")
|
|
549
549
|
.description("Loop Agent task and DAG automation CLI")
|
|
550
|
-
.version(
|
|
550
|
+
.version(readCliVersionLabel(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
|
|
551
551
|
.showHelpAfterError()
|
|
552
552
|
.option("--repo-root <path>", "repository root")
|
|
553
553
|
.option("--adapter <name>", "harness adapter name")
|
|
@@ -520,6 +520,33 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
520
520
|
}
|
|
521
521
|
let result;
|
|
522
522
|
try {
|
|
523
|
+
// Phase 5 (P1-9): capture the writeSet baseline BEFORE the writer provider
|
|
524
|
+
// call so a transient partial write can be rolled back and recovered via a
|
|
525
|
+
// staged child. Only frontend-implementation writers; a failed capture is a
|
|
526
|
+
// hard fail (no baseline → no rollback → no recovery). Dynamic import avoids
|
|
527
|
+
// an executor ↔ scheduler static cycle.
|
|
528
|
+
if (isWriteTask &&
|
|
529
|
+
(input.task.id === "frontend-implement-pi" ||
|
|
530
|
+
input.task.id === "frontend-repair-pi") &&
|
|
531
|
+
(input.task.writeSet?.length ?? 0) > 0) {
|
|
532
|
+
try {
|
|
533
|
+
const { captureFrontendWriterAttemptIntent } = await import("../workflows/dag/frontend-writer-recovery.js");
|
|
534
|
+
await captureFrontendWriterAttemptIntent({
|
|
535
|
+
cwd: input.cwd,
|
|
536
|
+
runDir: meta.runDir,
|
|
537
|
+
writeSet: input.task.writeSet,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
stdout: "",
|
|
544
|
+
stderr: `frontend writer attempt baseline capture failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
545
|
+
failureCategory: "write-guard",
|
|
546
|
+
durationMs: Date.now() - started,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
523
550
|
result = await piStepFn({
|
|
524
551
|
attachedFiles: [],
|
|
525
552
|
modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
|
|
@@ -691,6 +718,23 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
691
718
|
const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
|
|
692
719
|
changeManifestAfterStatus = afterStatus;
|
|
693
720
|
changeManifestChangedFiles = changedFiles;
|
|
721
|
+
// Phase 5: record the attempt changed paths/hashes into the rollback
|
|
722
|
+
// intent so a transient partial write can be CAS-restored.
|
|
723
|
+
if ((input.task.id === "frontend-implement-pi" ||
|
|
724
|
+
input.task.id === "frontend-repair-pi") &&
|
|
725
|
+
(input.task.writeSet?.length ?? 0) > 0) {
|
|
726
|
+
try {
|
|
727
|
+
const { recordFrontendWriterAttempt } = await import("../workflows/dag/frontend-writer-recovery.js");
|
|
728
|
+
await recordFrontendWriterAttempt({
|
|
729
|
+
cwd: input.cwd,
|
|
730
|
+
runDir: meta.runDir,
|
|
731
|
+
changedPaths: changedFiles,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// best-effort: a missing intent (no baseline captured) is a no-op
|
|
736
|
+
}
|
|
737
|
+
}
|
|
694
738
|
const guard = validateShellWriteGuardFromDiff({
|
|
695
739
|
changedFiles,
|
|
696
740
|
task: input.task,
|
|
@@ -45,6 +45,48 @@ export function readPackageName(packageDir) {
|
|
|
45
45
|
}
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the build stamp (git SHA + build time) written by the build pipeline
|
|
50
|
+
* into `<packageDir>/dist/build-stamp.json`. `undefined` when absent (e.g.
|
|
51
|
+
* running from source via tsx, or a hand-assembled package), which callers
|
|
52
|
+
* treat as "no stamp available".
|
|
53
|
+
*/
|
|
54
|
+
export function readBuildStamp(packageDir) {
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync(path.join(packageDir, "dist", "build-stamp.json"), "utf-8");
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
if (parsed.schemaVersion !== 1 ||
|
|
59
|
+
typeof parsed.version !== "string" ||
|
|
60
|
+
typeof parsed.builtAt !== "string")
|
|
61
|
+
return undefined;
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Human-readable CLI version label: plain version, enriched with the build
|
|
70
|
+
* stamp (short git SHA + build date) when available so a stale dist/ that was
|
|
71
|
+
* built from an old commit is immediately recognizable in `--version`.
|
|
72
|
+
*/
|
|
73
|
+
export function readCliVersionLabel(packageDir) {
|
|
74
|
+
const version = readPackageVersion(packageDir) ?? "0.0.0";
|
|
75
|
+
const stamp = readBuildStamp(packageDir);
|
|
76
|
+
if (!stamp?.gitSha)
|
|
77
|
+
return version;
|
|
78
|
+
return `${version} (build ${stamp.gitSha.slice(0, 8)} ${stamp.builtAt.slice(0, 10)})`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Strip the build-stamp suffix from a CLI `--version` label, returning the bare
|
|
82
|
+
* SemVer. Controller-identity/preflight comparisons must use this rather than
|
|
83
|
+
* the raw label so a build-stamped `0.35.0 (build 7f8c5b5b …)` still matches
|
|
84
|
+
* `packageVersion === "0.35.0"`.
|
|
85
|
+
*/
|
|
86
|
+
export function normalizeCliVersionLabel(label) {
|
|
87
|
+
const match = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(label.trim());
|
|
88
|
+
return match ? match[0] : label.trim();
|
|
89
|
+
}
|
|
48
90
|
/**
|
|
49
91
|
* Compute the SHA-256 hash of a file on disk.
|
|
50
92
|
*
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assistant content-block helpers shared by Pi event mapping and Operator Chat UI.
|
|
3
|
+
* Mirrors pi-web `lib/message-display.ts` (process vs answer split) without deferred fetch.
|
|
4
|
+
*/
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return undefined;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function readString(value) {
|
|
11
|
+
return typeof value === "string" ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Parse a persisted/SSE content array; unknown shapes are dropped. */
|
|
14
|
+
export function parseAssistantContent(value) {
|
|
15
|
+
if (!Array.isArray(value))
|
|
16
|
+
return undefined;
|
|
17
|
+
const blocks = [];
|
|
18
|
+
for (const item of value) {
|
|
19
|
+
const rec = asRecord(item);
|
|
20
|
+
if (!rec)
|
|
21
|
+
continue;
|
|
22
|
+
const type = readString(rec.type) ?? "";
|
|
23
|
+
if (type === "thinking" || type === "reasoning") {
|
|
24
|
+
const thinking = readString(rec.thinking) ?? readString(rec.text) ?? "";
|
|
25
|
+
blocks.push({ type: "thinking", thinking });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (type === "toolCall" ||
|
|
29
|
+
type === "tool_call" ||
|
|
30
|
+
type === "toolUse" ||
|
|
31
|
+
type === "tool_use") {
|
|
32
|
+
const toolCallId = readString(rec.toolCallId) ?? readString(rec.id) ?? "";
|
|
33
|
+
if (!toolCallId)
|
|
34
|
+
continue;
|
|
35
|
+
const toolName = readString(rec.toolName) ?? readString(rec.name);
|
|
36
|
+
blocks.push({
|
|
37
|
+
type: "toolCall",
|
|
38
|
+
toolCallId,
|
|
39
|
+
...(toolName ? { toolName } : {}),
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (type === "text" || type === "output_text" || type === "") {
|
|
44
|
+
const text = readString(rec.text);
|
|
45
|
+
if (text !== undefined)
|
|
46
|
+
blocks.push({ type: "text", text });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract ordered assistant content blocks from a Pi SDK message object.
|
|
53
|
+
* Thinking/reasoning blocks keep their own type so they never leak into answer text.
|
|
54
|
+
*/
|
|
55
|
+
export function extractAssistantContentBlocks(message) {
|
|
56
|
+
const rec = asRecord(message);
|
|
57
|
+
if (!rec)
|
|
58
|
+
return [];
|
|
59
|
+
const contents = Array.isArray(rec.content) ? rec.content : [];
|
|
60
|
+
const parts = Array.isArray(rec.parts) ? rec.parts : [];
|
|
61
|
+
const fromArrays = parseAssistantContent([...contents, ...parts]) ?? [];
|
|
62
|
+
if (fromArrays.length > 0)
|
|
63
|
+
return fromArrays;
|
|
64
|
+
const top = readString(rec.text);
|
|
65
|
+
return top ? [{ type: "text", text: top }] : [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Pi emits message_end for user / toolResult / custom too. Only assistant
|
|
69
|
+
* payloads belong in the Operator Chat assistant bubble (pi-web same split).
|
|
70
|
+
*/
|
|
71
|
+
export function isAssistantSdkMessage(message) {
|
|
72
|
+
const rec = asRecord(message);
|
|
73
|
+
if (!rec)
|
|
74
|
+
return false;
|
|
75
|
+
const role = readString(rec.role);
|
|
76
|
+
return role === undefined || role === "assistant";
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Latest assistant answer text (text blocks only). Thinking is excluded.
|
|
80
|
+
* Preserves the historical `{ text }` / `{ parts: [{ text }] }` fallbacks.
|
|
81
|
+
*/
|
|
82
|
+
export function extractAssistantText(message) {
|
|
83
|
+
return extractAssistantContentBlocks(message)
|
|
84
|
+
.filter((block) => block.type === "text")
|
|
85
|
+
.map((block) => block.text)
|
|
86
|
+
.join("");
|
|
87
|
+
}
|
|
88
|
+
export function isEmptyThinkingBlock(block, options = {}) {
|
|
89
|
+
return (block.type === "thinking" &&
|
|
90
|
+
!options.isStreaming &&
|
|
91
|
+
block.thinking.trim() === "");
|
|
92
|
+
}
|
|
93
|
+
export function getDisplayableAssistantBlocks(blocks, options = {}) {
|
|
94
|
+
return blocks.filter((block) => !isEmptyThinkingBlock(block, options));
|
|
95
|
+
}
|
|
96
|
+
function isFinalAnswerBlock(block) {
|
|
97
|
+
return block.type === "text";
|
|
98
|
+
}
|
|
99
|
+
export function splitFinalAssistantBlocks(blocks, options = {}) {
|
|
100
|
+
const displayable = getDisplayableAssistantBlocks(blocks, options);
|
|
101
|
+
let lastProcessIndex = -1;
|
|
102
|
+
for (let i = displayable.length - 1; i >= 0; i--) {
|
|
103
|
+
if (!isFinalAnswerBlock(displayable[i])) {
|
|
104
|
+
lastProcessIndex = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (lastProcessIndex === -1) {
|
|
109
|
+
return { answerBlocks: displayable, processBlocks: [] };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
answerBlocks: displayable.slice(lastProcessIndex + 1),
|
|
113
|
+
processBlocks: displayable.slice(0, lastProcessIndex + 1),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function joinAssistantText(blocks) {
|
|
117
|
+
return blocks
|
|
118
|
+
.filter((block) => block.type === "text")
|
|
119
|
+
.map((block) => block.text)
|
|
120
|
+
.join("");
|
|
121
|
+
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
29
29
|
import { access } from "node:fs/promises";
|
|
30
30
|
import path from "node:path";
|
|
31
|
+
import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
|
|
31
32
|
import { projectCompactSnapshot, } from "./chat-event-store.js";
|
|
32
33
|
import { extractUsageSample } from "./usage.js";
|
|
33
34
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
@@ -563,28 +564,7 @@ export function projectRuntimeSnapshot(input) {
|
|
|
563
564
|
hasBash: input.hasBash,
|
|
564
565
|
};
|
|
565
566
|
}
|
|
566
|
-
|
|
567
|
-
* Extract the latest assistant text fragment from an SDK message object.
|
|
568
|
-
* Tolerant of multiple message shapes the SDK emits.
|
|
569
|
-
*/
|
|
570
|
-
export function extractAssistantText(message) {
|
|
571
|
-
if (!message || typeof message !== "object")
|
|
572
|
-
return "";
|
|
573
|
-
const m = message;
|
|
574
|
-
if (typeof m.text === "string")
|
|
575
|
-
return m.text;
|
|
576
|
-
const contents = Array.isArray(m.content) ? m.content : [];
|
|
577
|
-
const parts = Array.isArray(m.parts) ? m.parts : [];
|
|
578
|
-
const segments = [];
|
|
579
|
-
for (const c of [...contents, ...parts]) {
|
|
580
|
-
if (!c || typeof c !== "object")
|
|
581
|
-
continue;
|
|
582
|
-
const seg = c;
|
|
583
|
-
if (typeof seg.text === "string")
|
|
584
|
-
segments.push(seg.text);
|
|
585
|
-
}
|
|
586
|
-
return segments.join("");
|
|
587
|
-
}
|
|
567
|
+
export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
|
|
588
568
|
function readSessionManagerId(manager) {
|
|
589
569
|
if (!manager || typeof manager !== "object")
|
|
590
570
|
return undefined;
|
|
@@ -2116,6 +2096,15 @@ export class ConsolePiRuntime {
|
|
|
2116
2096
|
this.dispose(id);
|
|
2117
2097
|
}
|
|
2118
2098
|
}
|
|
2099
|
+
function mapAssistantMessageEvent(type, sessionId, message) {
|
|
2100
|
+
const content = extractAssistantContentBlocks(message);
|
|
2101
|
+
return {
|
|
2102
|
+
type,
|
|
2103
|
+
sessionId,
|
|
2104
|
+
text: extractAssistantText(message),
|
|
2105
|
+
...(content.length > 0 ? { content } : {}),
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2119
2108
|
function mapSdkEvent(sessionId, event) {
|
|
2120
2109
|
switch (event.type) {
|
|
2121
2110
|
case "agent_start":
|
|
@@ -2127,17 +2116,13 @@ function mapSdkEvent(sessionId, event) {
|
|
|
2127
2116
|
willRetry: Boolean(event.willRetry),
|
|
2128
2117
|
};
|
|
2129
2118
|
case "message_update":
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
text: extractAssistantText(event.message),
|
|
2134
|
-
};
|
|
2119
|
+
if (!isAssistantSdkMessage(event.message))
|
|
2120
|
+
return undefined;
|
|
2121
|
+
return mapAssistantMessageEvent("message_update", sessionId, event.message);
|
|
2135
2122
|
case "message_end":
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
text: extractAssistantText(event.message),
|
|
2140
|
-
};
|
|
2123
|
+
if (!isAssistantSdkMessage(event.message))
|
|
2124
|
+
return undefined;
|
|
2125
|
+
return mapAssistantMessageEvent("message_end", sessionId, event.message);
|
|
2141
2126
|
case "turn_end":
|
|
2142
2127
|
// Usage is emitted from message_end (see prompt subscribe). turn_end
|
|
2143
2128
|
// carries the same final assistant message and would double-count.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Mutation routes go through the Console mutation gate (boot token + Origin).
|
|
13
13
|
* Read routes (GET capabilities / sessions) are open to loopback.
|
|
14
14
|
*/
|
|
15
|
+
import { parseAssistantContent } from "./assistant-content.js";
|
|
15
16
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
|
|
16
17
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
17
18
|
import { cleanChatTitle } from "./session-store.js";
|
|
@@ -83,12 +84,16 @@ function turnEventToStorePartial(event) {
|
|
|
83
84
|
case "agent_start":
|
|
84
85
|
return { kind: "agent_start", data: {} };
|
|
85
86
|
case "message_update":
|
|
87
|
+
case "message_end": {
|
|
88
|
+
const content = parseAssistantContent(event.content);
|
|
86
89
|
return {
|
|
87
|
-
kind:
|
|
88
|
-
data: {
|
|
90
|
+
kind: event.type,
|
|
91
|
+
data: {
|
|
92
|
+
text: String(event.text ?? ""),
|
|
93
|
+
...(content ? { content } : {}),
|
|
94
|
+
},
|
|
89
95
|
};
|
|
90
|
-
|
|
91
|
-
return { kind: "message_end", data: { text: String(event.text ?? "") } };
|
|
96
|
+
}
|
|
92
97
|
case "usage":
|
|
93
98
|
return { kind: "usage", data: (event.usage ?? {}) };
|
|
94
99
|
case "tool_call":
|
|
@@ -368,6 +368,9 @@ export async function runChatTurn(options) {
|
|
|
368
368
|
pending.push(store.appendMessage(sessionId, {
|
|
369
369
|
role: "assistant",
|
|
370
370
|
text: event.text,
|
|
371
|
+
...(event.content && event.content.length > 0
|
|
372
|
+
? { content: event.content }
|
|
373
|
+
: {}),
|
|
371
374
|
}));
|
|
372
375
|
}
|
|
373
376
|
break;
|
|
@@ -41,13 +41,6 @@ export const SHORTCUTS = [
|
|
|
41
41
|
action: "reload",
|
|
42
42
|
source: "operator",
|
|
43
43
|
},
|
|
44
|
-
{
|
|
45
|
-
command: "/help",
|
|
46
|
-
label: "Shortcuts",
|
|
47
|
-
description: "显示可用 shortcut",
|
|
48
|
-
action: "show-help",
|
|
49
|
-
source: "operator",
|
|
50
|
-
},
|
|
51
44
|
];
|
|
52
45
|
/** Pi Web commands with real browser-equivalent behavior (UI-11). */
|
|
53
46
|
export const WEB_SLASH_COMMANDS = [
|
|
@@ -103,6 +96,15 @@ export function resolveShortcut(input) {
|
|
|
103
96
|
return { command: local.command, action: local.action };
|
|
104
97
|
return { command: token, action: "unknown" };
|
|
105
98
|
}
|
|
99
|
+
/** Map Operator panel shortcuts onto Console workspace routes. */
|
|
100
|
+
export function workspaceForShortcut(action) {
|
|
101
|
+
if (action === "focus-panel:task" || action === "focus-panel:contract") {
|
|
102
|
+
return "tasks";
|
|
103
|
+
}
|
|
104
|
+
if (action === "focus-panel:dag")
|
|
105
|
+
return "recovery";
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
106
108
|
/**
|
|
107
109
|
* Merge Operator, Web, and dynamic Session commands with conflict dedupe.
|
|
108
110
|
* Priority: operator/web > extension > skill > prompt. /reload is single-card.
|