This repository has no description
1"""Issue session context injected before the agent runs (no issue-fetch tools)."""
2
3from __future__ import annotations
4
5import json
6from dataclasses import asdict, dataclass, field
7from typing import Any
8
9
10@dataclass
11class IssueSessionContext:
12 """Everything the caller already knows about the issue + repo."""
13
14 issue_uri: str
15 issue_rkey: str
16 title: str
17 body: str
18 state: str
19 author_did: str
20 author_handle: str
21 repo_did: str
22 repo_owner_handle: str
23 repo_name: str
24 knot_hostname: str
25 # Repo paths relative to root (provided by caller — primary navigation aid).
26 file_tree: list[str] = field(default_factory=list)
27 ref: str = "HEAD"
28 extra: dict[str, Any] = field(default_factory=dict)
29
30 @classmethod
31 def from_dict(cls, data: dict[str, Any]) -> IssueSessionContext:
32 known = {f.name for f in cls.__dataclass_fields__.values()} # type: ignore[attr-defined]
33 core = {k: v for k, v in data.items() if k in known and k != "extra"}
34 extra = dict(data.get("extra") or {})
35 for k, v in data.items():
36 if k not in known:
37 extra[k] = v
38 return cls(**core, extra=extra)
39
40 def to_dict(self) -> dict[str, Any]:
41 payload = asdict(self)
42 extra = payload.pop("extra", {})
43 if extra:
44 payload.update(extra)
45 return payload
46
47
48ISSUE_AGENT_SYSTEM_PROMPT = """\
49You investigate a single Tangled issue. The issue metadata, repo identifiers, and
50repository file tree are already provided below — do not ask the user to resolve
51handles or DIDs.
52
53Your job:
541. Read the issue title/body and identify which files are relevant.
552. Use ``read_repo_file`` to pull exact source from the knot when you need code.
563. Use ``list_repo_files`` only if the provided file tree is incomplete or you
57 need to explore a subdirectory that was not listed.
58
59Rules:
60- Prefer paths from the provided file tree.
61- Read the smallest set of files needed to answer well.
62- Cite file paths when referencing code.
63- You cannot file issues, push code, or browse outside this repo.
64"""
65
66
67def format_issue_context_block(ctx: IssueSessionContext) -> str:
68 """Serialize session context for the system prompt (cache-friendly static prefix)."""
69 tree = ctx.file_tree
70 if len(tree) > 500:
71 tree_display = tree[:500] + [f"... (+{len(tree) - 500} more paths)"]
72 else:
73 tree_display = tree
74
75 block = {
76 "issue": {
77 "uri": ctx.issue_uri,
78 "rkey": ctx.issue_rkey,
79 "title": ctx.title,
80 "body": ctx.body,
81 "state": ctx.state,
82 "author": {"did": ctx.author_did, "handle": ctx.author_handle},
83 },
84 "repo": {
85 "did": ctx.repo_did,
86 "owner_handle": ctx.repo_owner_handle,
87 "name": ctx.repo_name,
88 "knot_hostname": ctx.knot_hostname,
89 "ref": ctx.ref,
90 },
91 "file_tree": tree_display,
92 }
93 if ctx.extra:
94 block["extra"] = ctx.extra
95 return json.dumps(block, indent=2, ensure_ascii=False)
96
97
98def build_issue_system_prompt(ctx: IssueSessionContext) -> str:
99 return (
100 f"{ISSUE_AGENT_SYSTEM_PROMPT}\n\n"
101 f"## Session context (issue + repo)\n\n"
102 f"```json\n{format_issue_context_block(ctx)}\n```"
103 )