From 775189d9160fa483c8687fc92eb0389f4bf9200b Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:09:01 -0400 Subject: [PATCH] =?UTF-8?q?feat(digest):=20related=5Ftasks=20=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E6=8A=98=E5=8F=A0(=C2=A72=20=E8=B7=A8=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关联/继承任务的概览现在复用其【自身的折叠视图】,与当前任务同逻辑: - relatedTaskOverviews 按 source 任务自己的 covered/hot 折出已折成员(recent_facts / recent_intent_results 跳过已折且仍冷的成员),并把该任务的 cold_digests(body)只读带出, 统一 inherited=true / source_task_id 标记。 - expand_digest 支持跨任务:先查本任务,再查直接关联任务(read-only),返回成员紧凑列表并 标继承;node_detail 已有的 GetNodeWithSources 负责成员完整还原。 - 复活检查同样生效:source 任务里已复活(转热)的被覆盖成员照常显示,不被折出。 至此设计文档 §2 跨任务折叠全部落地;仅 §2.3 墙钟兜底(任务停摆判稳)按既定作为后续。 --- agent/tools.go | 24 +++++++++++- agent/tools_digest.go | 85 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/agent/tools.go b/agent/tools.go index ad80fcb..2e89656 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -810,6 +810,9 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any { out := make([]map[string]any, 0, len(sources)) for _, source := range sources { ts := source.Store + // §2 cross-task: render the source task's OWN folded view — fold out the + // members it has already folded, and surface its cold_digests read-only. + hidden := hiddenMembersFor(ts) budget := overviewTextBudget{remaining: perSourceTextBudget} item := map[string]any{ "source_task_id": source.Task.TaskID, @@ -884,6 +887,9 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any { item["recent_findings"] = recentFindings recentFacts := make([]map[string]any, 0, len(facts)) for _, fact := range facts { + if hidden(fact.ID) { + continue // folded into this source's cold_digests — shown there (§2/§6.2) + } m := inheritedMap(compactNode(fact), source.Task.TaskID) m["summary"] = budget.take(m["summary"], 400) if from := factFrom[fact.ID]; from > 0 && terminalIntent[from] { @@ -899,7 +905,14 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any { } item["recent_facts"] = recentFacts - recentDone := recentTerminalIntents(ts, relatedOverviewMaxIntentsPerTask) + recentDoneRaw := recentTerminalIntents(ts, relatedOverviewMaxIntentsPerTask) + recentDone := recentDoneRaw[:0] // in-place filter: drop this source's folded intents (§2) + for _, intent := range recentDoneRaw { + if hidden(intent.ID) { + continue + } + recentDone = append(recentDone, intent) + } for _, intent := range recentDone { intent.Inherited = true intent.SourceTaskID = source.Task.TaskID @@ -928,6 +941,15 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any { } } item["recent_intent_results"] = intentResults + // §2 cross-task: the source task's folded cold region, read-only. Members are + // resolvable via expand_digest(id)/node_detail(id), which search source tasks. + if cds := activeDigestBodies(ts); len(cds) > 0 { + for _, cd := range cds { + cd["inherited"] = true + cd["source_task_id"] = source.Task.TaskID + } + item["cold_digests"] = cds + } if statsErr == nil { item["node_stats"] = stats } diff --git a/agent/tools_digest.go b/agent/tools_digest.go index e83e832..1cf0e87 100644 --- a/agent/tools_digest.go +++ b/agent/tools_digest.go @@ -119,9 +119,10 @@ func (t *ToolSet) coldDigestOverview() (digests []map[string]any, index []map[st } // digestMemberEntry builds the compact per-member view expand_digest returns — -// same shape as recent_facts / recent_done_intents (§6.1 middle level). -func (t *ToolSet) digestMemberEntry(id int64) map[string]any { - n, _ := t.ts.GetNode(id) +// same shape as recent_facts / recent_done_intents (§6.1 middle level). store is +// the digest's OWNING store (the current task, or a read-only source task §2). +func (t *ToolSet) digestMemberEntry(store *db.ExplorationStore, id int64) map[string]any { + n, _ := store.GetNode(id) if n == nil { return map[string]any{"id": id, "missing": true} } @@ -136,6 +137,60 @@ func (t *ToolSet) digestMemberEntry(id int64) map[string]any { return m } +// activeDigestBodies returns [{id, body, member_count}] for a store's active +// digests — the folded cold region as flat bodies (§6.1). Shared by the current +// task overview and the read-only related-task overview (§2 cross-task reuse). +func activeDigestBodies(store *db.ExplorationStore) []map[string]any { + ads, err := store.ActiveDigests() + if err != nil || len(ads) == 0 { + return nil + } + out := make([]map[string]any, 0, len(ads)) + for _, d := range ads { + var p struct { + Body string `json:"body"` + } + _ = json.Unmarshal(d.Payload, &p) + ms, _ := store.DigestMembers(d.ID) + out = append(out, map[string]any{"id": d.ID, "body": p.Body, "member_count": len(ms)}) + } + return out +} + +// hiddenMembersFor returns a predicate telling whether a member is hidden (folded +// into an active digest AND still cold) in the given store — so a source task's +// overview folds exactly the way that task folds itself (§2 cross-task: "当前任务 +// 什么展示逻辑,关联任务就什么逻辑"). A revived (now hot) covered member is NOT +// hidden (§6 render-time revival check). Returns a never-hidden predicate when the +// store has no digests. +func hiddenMembersFor(store *db.ExplorationStore) func(int64) bool { + covered, err := store.CoveredMembers() + if err != nil || len(covered) == 0 { + return func(int64) bool { return false } + } + var hot map[int64]bool + if cg, _, err := loadColdGraph(store); err == nil { + hot = cg.hotSet() + } + return func(id int64) bool { _, c := covered[id]; return c && !hot[id] } +} + +// resolveDigest finds a digest node by id in the current task, else in a direct +// source task (read-only, §2). Returns the node, its owning store, and the source +// task id (0 = current task). +func (t *ToolSet) resolveDigest(id int64) (*db.Node, *db.ExplorationStore, int64) { + if n, _ := t.ts.GetNode(id); n != nil && n.Kind == db.KindDigest { + return n, t.ts, 0 + } + srcs, _ := t.ts.DirectSourceStores() + for _, s := range srcs { + if n, _ := s.Store.GetNode(id); n != nil && n.Kind == db.KindDigest { + return n, s.Store, s.Task.TaskID + } + } + return nil, nil, 0 +} + // expandDigest returns a digest's covered members as a compact list (§6.1). It is // a distinct tool from node_detail because it returns a LIST of members, not one // node's full detail. @@ -154,25 +209,35 @@ func (t *ToolSet) expandDigest() actool.CoreTool { ID int64 `json:"id"` } _ = json.Unmarshal(raw, &in) - n, _ := t.ts.GetNode(in.ID) - if n == nil || n.Kind != db.KindDigest { - return jsonResult(map[string]any{"error": fmt.Sprintf("#%d 不是 digest 节点", in.ID)}) + n, store, srcTaskID := t.resolveDigest(in.ID) + if n == nil { + return jsonResult(map[string]any{"error": fmt.Sprintf("#%d 不是 digest 节点(本任务或直接关联任务里都没找到)", in.ID)}) } var p struct { Body string `json:"body"` } _ = json.Unmarshal(n.Payload, &p) - members, _ := t.ts.DigestMembers(in.ID) + members, _ := store.DigestMembers(in.ID) list := make([]map[string]any, 0, len(members)) for _, m := range members { - list = append(list, t.digestMemberEntry(m)) + entry := t.digestMemberEntry(store, m) + if srcTaskID > 0 { // 关联任务的成员:只读,带继承标记(§2) + entry["inherited"] = true + entry["source_task_id"] = srcTaskID + } + list = append(list, entry) } - return jsonResult(map[string]any{ + out := map[string]any{ "id": in.ID, "state": n.State, // active / superseded "body": p.Body, "members": list, - }) + } + if srcTaskID > 0 { + out["inherited"] = true + out["source_task_id"] = srcTaskID + } + return jsonResult(out) }) }