Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions desktop/scripts/prepare-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ const bundleFlavor = process.env.HUGAGENT_DESKTOP_BUNDLE || "full";
if (!["full", "thin"].includes(bundleFlavor)) {
throw new Error(`Unknown HUGAGENT_DESKTOP_BUNDLE flavor: ${bundleFlavor}`);
}
validateDesktopBuildTarget(
process.env.TAURI_ENV_TARGET_TRIPLE
? ["--target", process.env.TAURI_ENV_TARGET_TRIPLE]
: [],
);
if (bundleFlavor === "full") {
// The offline runtime is a native payload, so a cross-architecture target
// cannot be packaged with it. Thin bundles carry no runtime — any target,
// including universal-apple-darwin, is buildable on any host.
validateDesktopBuildTarget(
process.env.TAURI_ENV_TARGET_TRIPLE
? ["--target", process.env.TAURI_ENV_TARGET_TRIPLE]
: [],
);
}
const desktopTarget = currentDesktopTarget();
const dependencyFingerprint = desktopDependencyFingerprint(repoRoot, desktopTarget);
const python = bundleFlavor === "full" ? findPython() : null;
Expand Down
56 changes: 54 additions & 2 deletions src/backend/core/evolution/user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,23 @@ def pending_for_user(user_id: str, *, limit: int = 20) -> List[Dict[str, Any]]:
action = personal_action(candidate)
if action is None:
continue
change_preview = _change_preview(candidate)
if str(candidate.target_kind or "") == "skill":
# Legacy personal cycles could promote a single repeated
# tool call as a "sequence". Such a candidate has neither
# an order to preserve nor an applicability condition, and
# approving it would install a tautological skill. Keep
# the evidence intact for audit, but do not present noise as
# a decision the user ought to make.
if change_preview.get("type") == "skill_sequence":
steps = change_preview.get("steps") or []
rules = change_preview.get("ordering_constraints") or []
if len(steps) < 2 and not rules:
continue
elif change_preview.get("type") != "skill_document":
# A skill with no inspectable content is not informed
# consent and would also fail materialisation later.
continue
out.append(
{
"candidate_id": candidate.candidate_id,
Expand All @@ -276,7 +293,7 @@ def pending_for_user(user_id: str, *, limit: int = 20) -> List[Dict[str, Any]]:
"action": action["action"],
"action_label": action["label"],
"action_effect": action["effect"],
"change": _change_preview(candidate),
"change": change_preview,
"created_at": candidate.created_at.isoformat()
if candidate.created_at
else None,
Expand All @@ -300,7 +317,8 @@ def _change_preview(candidate) -> Dict[str, Any]:
"""
ir = candidate.ir or {}
kind = str(candidate.target_kind or "")
for change in ir.get("changes") or []:
changes = ir.get("changes") or []
for change in changes:
if kind == "skill":
document = change.get("document")
if isinstance(document, dict) and document.get("content"):
Expand All @@ -327,6 +345,40 @@ def _change_preview(candidate) -> Dict[str, Any]:
for op in operations
],
}
if kind == "skill":
# Sequence-only candidates are the common output of the personal
# evolution path. They do not carry an authored SKILL.md, but approval
# still materialises a concrete name, description and ordered set of
# steps. Surface that exact structure instead of returning {}, which
# made the frontend silently remove the detail button.
tools: List[str] = []
constraints: List[Dict[str, Any]] = []
for change in changes:
sequence = change.get("tool_sequence")
if isinstance(sequence, list) and sequence and not tools:
tools = [str(tool) for tool in sequence]
rules = change.get("ordering_constraints")
if isinstance(rules, list) and rules:
constraints = [dict(rule) for rule in rules if isinstance(rule, dict)]

rules_only = not tools and bool(constraints)
if rules_only:
from core.evolution.activation import _tools_from_constraints

tools = _tools_from_constraints(constraints)
if tools:
# Reuse the materialiser's wording so the preview and the installed
# skill never disagree about what the user accepted.
from core.evolution.activation import _skill_description, _skill_label

return {
"type": "skill_sequence",
"display_name": _skill_label(tools, rules_only=rules_only),
"description": _skill_description(tools, constraints, rules_only=rules_only),
"allowed_tools": tools,
"steps": tools,
"ordering_constraints": constraints,
}
return {}


Expand Down
20 changes: 17 additions & 3 deletions src/backend/core/memory/extractors/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,28 @@
- graph: 稳定的实体关系(隶属、负责、依赖、使用、组成、别名、分类)
- task: 本轮明确提出的多步任务目标(仅本会话内使用)

【判定标准 —— 宁缺毋滥
绝大多数对话轮次**没有**值得长期记住的内容。以下都不算
【判定标准】
以下内容都不算
- 一次性的问答、查询、闲聊
- 只对当前这一次请求有效的指令("这次用表格""先看第三页")
- 助手自己说的内容(除非用户明确认可为约定)
- 模型本来就会的通用常识

只有当**下次对话不知道这条信息就会做错或重复问**时,才算值得记住。
以下内容应当记住:
- 用户明确要求“记住”“记录一下”“以后”“下次”“默认”“一律”“不要再”,并给出需要延续的信息;明确说明仅本次有效的除外
- 用户直接陈述或确认的身份、岗位、部门、单位、职责、联系方式等相对稳定的信息 → identity
- 用户反复适用的表达与交付偏好,例如语言、详略、格式、称呼、禁忌和默认输出方式 → preference
- 用户所在组织、团队或项目特有的可复用规则,例如术语定义、统计口径、计算方法、步骤顺序、命名规范、目录约定、必做校验、交付标准和风险红线 → procedural
- 用户直接陈述或确认的稳定实体关系,例如谁负责什么、哪个部门隶属哪里、系统依赖什么、产品由哪些模块组成、名称与别名的对应关系 → graph
- 用户明确提出的多步任务,以及尚未完成的目标、步骤、进度、截止时间和交付物 → task;此类信息只在本会话内保留,不作为长期事实
- 用户对已有信息的纠正、替换或失效声明,例如“不是 A,是 B”“我现在改负责 X”“以后不用旧口径”;应记住新信息并用于更新旧记忆
- 助手提出的方案被用户明确确认为今后的规则或偏好,例如“对,以后都按这个执行”;按对应类别记住

总判断原则:
- 只有用户明确说出或明确确认的内容才能作为事实写入,不要把助手的推测当成用户记忆
- 判断它在未来是否仍然有用:如果下次对话不知道这条信息会导致做错、重复询问或违反用户约定,就应该记住
- 用户明确要求记住时应优先保留;若内容同时符合多个类别,可以返回多个类别
- 不要因为信息只出现一次就拒绝;稳定性由语义和用户意图判断,而不是由出现次数判断

【输出格式(严格 JSON,无代码块包裹)】
{{"classes": ["identity", "procedural"]}}
Expand Down
28 changes: 28 additions & 0 deletions src/backend/tests/evolution/test_personal_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,34 @@ def test_the_tool_sequence_is_surfaced_so_approval_is_informed(db):
assert pending[0]["tool_sequence"] == ["a", "b"]


def test_sequence_only_skill_has_an_inspectable_change_preview(db):
for i in range(5):
_episode(db, f"e{i}", "alice")
_candidate(db, "c1", [f"e{i}" for i in range(5)])
db.commit()

change = US.pending_for_user("alice")[0]["change"]
assert change["type"] == "skill_sequence"
assert change["display_name"] == "固定调用顺序:a → b"
assert change["steps"] == ["a", "b"]
assert "`a`、`b`" in change["description"]


def test_legacy_single_tool_candidate_is_hidden_without_deleting_evidence(db):
for i in range(5):
_episode(db, f"e{i}", "alice")
_candidate(
db,
"single",
[f"e{i}" for i in range(5)],
ir={"changes": [{"tool_sequence": ["view_text_file"]}]},
)
db.commit()

assert US.pending_for_user("alice") == []
assert db.get(EvolutionCandidate, "single") is not None


# ── The queue lists only what it can carry out ───────────────────────────────
#
# Every case below used to appear with a 「为我启用」 button whose sole possible
Expand Down
18 changes: 18 additions & 0 deletions src/backend/tests/evolution/test_promotion_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ def test_recurring_pattern_is_compiled_into_a_skill_proposal():
assert "重新规划" not in proposal.rationale


def test_one_repeated_tool_call_is_not_mislabelled_as_a_sequence_skill():
pattern = P.Pattern(
kind=P.PATTERN_SUCCESS_SUBSEQUENCE,
signature="call_subagent",
support=9,
success_rate=1.0,
tool_sequence=["call_subagent"],
episode_ids=[f"ep-{i}" for i in range(9)],
)

assert (
P.promote_tool_sequence_to_skill(
pattern, skill_credit=0.9, workflow_credit=0.1
)
is None
)


def test_promotion_is_refused_when_orchestration_explains_it_better():
proposal = P.promote_tool_sequence_to_skill(
_success_pattern(),
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3966,6 +3966,14 @@ export type EvolutionChangePreview =
allowed_tools: string[];
content: string;
}
| {
type: 'skill_sequence';
display_name: string;
description: string;
allowed_tools: string[];
steps: string[];
ordering_constraints: Array<Record<string, unknown>>;
}
| {
type: 'memory_ops';
operations: Array<{
Expand Down
Loading
Loading