agencode 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from textwrap import shorten
6
+ from typing import TYPE_CHECKING
7
+
8
+ from langchain_core.tools import tool
9
+
10
+ from agencli.agents.registry import AgentRegistry
11
+ from agencli.core.config import AgenCLIConfig
12
+ from agencli.skills.cli_backend import (
13
+ check_skills,
14
+ find_skills,
15
+ install_skill_cli,
16
+ list_installed_skills_cli,
17
+ render_command,
18
+ update_skills,
19
+ )
20
+ from agencli.skills.manager import install_skill as install_local_skill, list_installed_skills as list_local_skills
21
+
22
+ if TYPE_CHECKING:
23
+ from agencli.agents.factory import AgentSpec
24
+
25
+
26
+ def build_supervisor_management_tools(config: AgenCLIConfig) -> list:
27
+ registry = AgentRegistry(config.agents_dir)
28
+
29
+ @tool("list_saved_subagents")
30
+ def list_saved_subagents() -> str:
31
+ """List saved user-managed subagents and which ones are active under the supervisor."""
32
+ active = set(registry.load_active_names())
33
+ items = []
34
+ for name in registry.list_agents():
35
+ spec = registry.load(name)
36
+ items.append(
37
+ {
38
+ "name": spec.name,
39
+ "description": spec.description,
40
+ "skills": list(spec.skills),
41
+ "active": spec.name in active,
42
+ }
43
+ )
44
+ return json.dumps({"active_names": sorted(active), "subagents": items}, indent=2)
45
+
46
+ @tool("get_saved_subagent")
47
+ def get_saved_subagent(name: str) -> str:
48
+ """Load one saved subagent by name, including its prompt and attached skills."""
49
+ spec = registry.load(name.strip())
50
+ return json.dumps(_spec_payload(spec, active_names=registry.load_active_names()), indent=2)
51
+
52
+ @tool("save_subagent")
53
+ def save_subagent(
54
+ name: str,
55
+ system_prompt: str,
56
+ description: str = "",
57
+ skills: list[str] | None = None,
58
+ activate: bool = False,
59
+ ) -> str:
60
+ """Create or update a saved subagent with the given prompt and attached skill names."""
61
+ cleaned_name = name.strip()
62
+ cleaned_prompt = system_prompt.strip()
63
+ if not cleaned_name:
64
+ raise ValueError("Subagent name cannot be empty.")
65
+ if not cleaned_prompt:
66
+ raise ValueError("Subagent system prompt cannot be empty.")
67
+ from agencli.agents.factory import AgentSpec
68
+
69
+ existing = registry.load(cleaned_name) if cleaned_name in registry.list_agents() else None
70
+ spec = AgentSpec(
71
+ name=cleaned_name,
72
+ model=(existing.model if existing is not None else config.default_model),
73
+ system_prompt=cleaned_prompt,
74
+ description=description.strip() or (existing.description if existing is not None else _derive_description(cleaned_prompt)),
75
+ skills=sorted(dict.fromkeys(skills or (existing.skills if existing is not None else []))),
76
+ subagents=list(existing.subagents) if existing is not None else [],
77
+ mcp_servers=list(existing.mcp_servers) if existing is not None else [],
78
+ workspace_dir=(existing.workspace_dir if existing is not None else config.workspace_dir),
79
+ )
80
+ path = registry.save(spec)
81
+ active_names = set(registry.load_active_names())
82
+ if activate:
83
+ active_names.add(cleaned_name)
84
+ registry.save_active_names(sorted(active_names))
85
+ return json.dumps(
86
+ {
87
+ "saved": cleaned_name,
88
+ "path": str(path),
89
+ "active": cleaned_name in set(registry.load_active_names()),
90
+ "skills": spec.skills,
91
+ },
92
+ indent=2,
93
+ )
94
+
95
+ @tool("delete_subagent")
96
+ def delete_subagent(name: str) -> str:
97
+ """Delete a saved subagent and remove it from the supervisor active set."""
98
+ path = registry.delete(name.strip())
99
+ return json.dumps({"deleted": name.strip(), "path": str(path)}, indent=2)
100
+
101
+ @tool("set_active_subagents")
102
+ def set_active_subagents(names: list[str]) -> str:
103
+ """Replace the supervisor active subagent list with the provided saved subagent names."""
104
+ wanted = sorted({item.strip() for item in names if item and item.strip()})
105
+ existing = set(registry.list_agents())
106
+ missing = [name for name in wanted if name not in existing]
107
+ if missing:
108
+ raise ValueError(f"Unknown saved subagent(s): {', '.join(missing)}")
109
+ registry.save_active_names(wanted)
110
+ return json.dumps({"active_names": wanted}, indent=2)
111
+
112
+ @tool("list_attachable_skills")
113
+ def list_attachable_skills() -> str:
114
+ """List skills that can be attached to subagents, including local and Skills CLI installed skills."""
115
+ local = [
116
+ {
117
+ "name": skill.name,
118
+ "description": skill.description,
119
+ "source": str(skill.path),
120
+ "kind": "local",
121
+ }
122
+ for skill in list_local_skills(config.skills_dir)
123
+ ]
124
+ try:
125
+ _result, installed = list_installed_skills_cli(workspace_dir=config.workspace_dir)
126
+ except Exception as exc:
127
+ installed = []
128
+ local.append(
129
+ {
130
+ "name": "warning",
131
+ "description": f"Unable to query Skills CLI installed state: {exc}",
132
+ "source": "skills-cli",
133
+ "kind": "warning",
134
+ }
135
+ )
136
+ merged = {item["name"]: item for item in local}
137
+ for record in installed:
138
+ merged.setdefault(
139
+ record.name,
140
+ {
141
+ "name": record.name,
142
+ "description": record.details,
143
+ "source": record.source or "skills-cli",
144
+ "kind": f"installed:{record.scope or 'unknown'}",
145
+ },
146
+ )
147
+ return json.dumps({"skills": [merged[name] for name in sorted(merged)]}, indent=2)
148
+
149
+ @tool("search_marketplace_skills")
150
+ def search_marketplace_skills(query: str, limit: int = 8) -> str:
151
+ """Search the Skills marketplace with `npx skills find` and return the strongest matching skills."""
152
+ result, items = find_skills(query, workspace_dir=config.workspace_dir)
153
+ payload = {
154
+ "command": render_command(result.argv),
155
+ "ok": result.ok,
156
+ "results": [
157
+ {
158
+ "name": item.name,
159
+ "source": item.source,
160
+ "skill_name": item.skill_name,
161
+ "skills_page_url": item.skills_page_url,
162
+ "github_repo_url": item.github_repo_url,
163
+ "install_count": item.install_count_text,
164
+ "description": item.description,
165
+ "trust": list(item.quality.labels),
166
+ "install_command": render_command(item.install_command) if item.install_command else None,
167
+ }
168
+ for item in items[: max(1, limit)]
169
+ ],
170
+ }
171
+ return json.dumps(payload, indent=2)
172
+
173
+ @tool("install_marketplace_skill")
174
+ def install_marketplace_skill(source: str, skill_name: str = "", global_install: bool = True, yes: bool = True) -> str:
175
+ """Install a skill through the Skills CLI and sync it into the local attachable skill library when possible."""
176
+ result = install_skill_cli(
177
+ source,
178
+ workspace_dir=config.workspace_dir,
179
+ skill_name=skill_name.strip() or None,
180
+ global_install=global_install,
181
+ yes=yes,
182
+ )
183
+ imported = None
184
+ if result.ok and skill_name.strip():
185
+ imported = _sync_installed_skill_into_local_library(config, skill_name.strip())
186
+ return json.dumps(
187
+ {
188
+ "command": render_command(result.argv),
189
+ "ok": result.ok,
190
+ "imported_local_skill": imported,
191
+ "stdout": result.stdout.strip(),
192
+ "stderr": result.stderr.strip(),
193
+ "error": result.error_summary,
194
+ },
195
+ indent=2,
196
+ )
197
+
198
+ @tool("check_skill_updates")
199
+ def check_skill_updates() -> str:
200
+ """Check for updates to installed marketplace skills."""
201
+ result, status = check_skills(workspace_dir=config.workspace_dir)
202
+ return json.dumps(
203
+ {
204
+ "command": render_command(result.argv),
205
+ "ok": result.ok,
206
+ "summary": status.summary,
207
+ "items": status.items,
208
+ },
209
+ indent=2,
210
+ )
211
+
212
+ @tool("update_marketplace_skills")
213
+ def update_marketplace_skills() -> str:
214
+ """Update installed marketplace skills to their latest versions."""
215
+ result, status = update_skills(workspace_dir=config.workspace_dir)
216
+ return json.dumps(
217
+ {
218
+ "command": render_command(result.argv),
219
+ "ok": result.ok,
220
+ "summary": status.summary,
221
+ "items": status.items,
222
+ },
223
+ indent=2,
224
+ )
225
+
226
+ return [
227
+ list_saved_subagents,
228
+ get_saved_subagent,
229
+ save_subagent,
230
+ delete_subagent,
231
+ set_active_subagents,
232
+ list_attachable_skills,
233
+ search_marketplace_skills,
234
+ install_marketplace_skill,
235
+ check_skill_updates,
236
+ update_marketplace_skills,
237
+ ]
238
+
239
+
240
+ def _spec_payload(spec: "AgentSpec", *, active_names: list[str]) -> dict[str, object]:
241
+ return {
242
+ "name": spec.name,
243
+ "description": spec.description,
244
+ "system_prompt": spec.system_prompt,
245
+ "skills": list(spec.skills),
246
+ "active": spec.name in set(active_names),
247
+ "workspace_dir": spec.workspace_dir,
248
+ }
249
+
250
+
251
+ def _derive_description(system_prompt: str) -> str:
252
+ return shorten(system_prompt.strip().replace("\n", " "), width=96, placeholder="...")
253
+
254
+
255
+ def _sync_installed_skill_into_local_library(config: AgenCLIConfig, skill_name: str) -> str | None:
256
+ source_path = _discover_external_skill_path(config, skill_name)
257
+ if source_path is None:
258
+ return None
259
+ try:
260
+ installed = install_local_skill(source_path, config.skills_dir, overwrite=False)
261
+ return installed.name
262
+ except FileExistsError:
263
+ return skill_name
264
+
265
+
266
+ def _discover_external_skill_path(config: AgenCLIConfig, skill_name: str) -> str | None:
267
+ candidates = [
268
+ Path(config.workspace_dir) / ".agents" / "skills" / skill_name,
269
+ Path(config.workspace_dir) / ".codex" / "skills" / skill_name,
270
+ Path.home() / ".agents" / "skills" / skill_name,
271
+ Path.home() / ".codex" / "skills" / skill_name,
272
+ Path.home() / ".config" / "agents" / "skills" / skill_name,
273
+ ]
274
+ for candidate in candidates:
275
+ if (candidate / "SKILL.md").exists():
276
+ return candidate.as_posix()
277
+ return None
@@ -0,0 +1 @@
1
+ """Prebuilt agent definitions."""
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from agencli.agents.factory import AgentSpec
4
+ from agencli.agents.supervisor import build_supervisor_agent
5
+
6
+
7
+ PERFORMANCE_PROMPT_SUFFIX = (
8
+ " Optimize for fewer, larger operations. Inspect enough context up front, avoid repeating the same checks, "
9
+ "keep tool calls minimal, and prefer one strong command or grouped action when it can safely replace many "
10
+ "small steps."
11
+ )
12
+
13
+
14
+ def get_prebuilt_agents(default_model: str) -> dict[str, AgentSpec]:
15
+ agents = {
16
+ "filesystem-agent": AgentSpec(
17
+ name="filesystem-agent",
18
+ model=default_model,
19
+ description="Reads, writes, and navigates the workspace with deepagents' built-in filesystem tools.",
20
+ system_prompt=(
21
+ "You are a filesystem specialist. Read carefully before editing, "
22
+ "keep changes precise, and explain file operations clearly. When organizing files or folders, "
23
+ "prefer batched plans and grouped operations over one-item-at-a-time actions whenever the "
24
+ "available tools support it. Inspect first, decide the full target layout, then create needed "
25
+ "folders together and move related files in as few operations as possible to reduce tool chatter."
26
+ + PERFORMANCE_PROMPT_SUFFIX
27
+ ),
28
+ ),
29
+ "shell-agent": AgentSpec(
30
+ name="shell-agent",
31
+ model=default_model,
32
+ description="Runs shell commands with deepagents' built-in execute tool.",
33
+ system_prompt=(
34
+ "You are a shell automation specialist. Use commands carefully, "
35
+ "prefer safe inspections first, and summarize command results precisely. When shell automation "
36
+ "can safely batch a task, prefer one well-formed command over many tiny commands."
37
+ + PERFORMANCE_PROMPT_SUFFIX
38
+ ),
39
+ ),
40
+ "search-agent": AgentSpec(
41
+ name="search-agent",
42
+ model=default_model,
43
+ description="Searches the web through MCP-backed search tools.",
44
+ system_prompt=(
45
+ "You are a research specialist. Search broadly, synthesize findings, "
46
+ "and distinguish facts from inference. Avoid redundant searches and combine related questions into "
47
+ "the fewest high-value lookups that still produce a reliable answer."
48
+ + PERFORMANCE_PROMPT_SUFFIX
49
+ ),
50
+ mcp_servers=["search", "tavily-mcp"],
51
+ ),
52
+ "coding-agent": AgentSpec(
53
+ name="coding-agent",
54
+ model=default_model,
55
+ description="Combines deepagents' built-in filesystem and shell tools for implementation work.",
56
+ system_prompt=(
57
+ "You are an expert software engineer. Inspect files before editing, "
58
+ "make surgical changes, and verify your work. Prefer one cohesive patch and one targeted "
59
+ "verification pass over fragmented micro-steps. Use shell commands for bulk filesystem work when "
60
+ "they safely replace repetitive tool calls."
61
+ + PERFORMANCE_PROMPT_SUFFIX
62
+ ),
63
+ ),
64
+ }
65
+ agents["supervisor-agent"] = build_supervisor_agent(default_model)
66
+ return agents
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from agencli.agents.factory import AgentSpec
7
+
8
+
9
+ class AgentRegistry:
10
+ def __init__(self, agents_dir: str) -> None:
11
+ self.agents_path = Path(agents_dir)
12
+ self.agents_path.mkdir(parents=True, exist_ok=True)
13
+ self._state_path = self.agents_path / ".agencli-state.json"
14
+
15
+ def path_for(self, name: str) -> Path:
16
+ return self.agents_path / f"{name}.json"
17
+
18
+ def save(self, spec: AgentSpec) -> Path:
19
+ path = self.path_for(spec.name)
20
+ path.write_text(json.dumps(spec.serializable_dict(), indent=2), encoding="utf-8")
21
+ return path
22
+
23
+ def list_agents(self) -> list[str]:
24
+ return sorted(path.stem for path in self.agents_path.glob("*.json") if path.name != self._state_path.name)
25
+
26
+ def load(self, name: str) -> AgentSpec:
27
+ path = self.path_for(name)
28
+ raw = json.loads(path.read_text(encoding="utf-8"))
29
+ return AgentSpec.from_dict(raw)
30
+
31
+ def delete(self, name: str) -> Path:
32
+ path = self.path_for(name)
33
+ path.unlink()
34
+ active_names = [item for item in self.load_active_names() if item != name]
35
+ self.save_active_names(active_names)
36
+ return path
37
+
38
+ def load_active_names(self) -> list[str]:
39
+ if not self._state_path.exists():
40
+ return []
41
+ raw = json.loads(self._state_path.read_text(encoding="utf-8"))
42
+ names = raw.get("active_agents", [])
43
+ if not isinstance(names, list):
44
+ return []
45
+ return [str(name) for name in names if str(name).strip()]
46
+
47
+ def save_active_names(self, names: list[str]) -> Path:
48
+ normalized = sorted({name.strip() for name in names if name and name.strip()})
49
+ self._state_path.write_text(json.dumps({"active_agents": normalized}, indent=2), encoding="utf-8")
50
+ return self._state_path
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from typing import Any
6
+
7
+ from agencli.core.session import ChatTurn
8
+
9
+
10
+ DEFAULT_HISTORY_REPLAY_LIMIT = 50
11
+ TRACE_PREVIEW_LIMIT = 240
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class StreamEvent:
16
+ kind: str
17
+ label: str
18
+ text: str
19
+ phase: str = "update"
20
+ trace_id: str = ""
21
+
22
+
23
+ def build_prompt_payload(
24
+ prompt: str | None = None,
25
+ *,
26
+ history: list[ChatTurn] | None = None,
27
+ ) -> dict[str, Any]:
28
+ messages = build_message_history(history or [])
29
+ if prompt:
30
+ normalized_prompt = prompt.strip()
31
+ if normalized_prompt:
32
+ messages.append(
33
+ {
34
+ "role": "user",
35
+ "content": normalized_prompt,
36
+ }
37
+ )
38
+ return {"messages": messages}
39
+
40
+
41
+ def build_message_history(history: list[ChatTurn]) -> list[dict[str, str]]:
42
+ messages: list[dict[str, str]] = []
43
+ for turn in history:
44
+ role = _normalize_role(turn.role)
45
+ content = turn.content.strip()
46
+ if role is None or not content:
47
+ continue
48
+ messages.append({"role": role, "content": content})
49
+ return messages
50
+
51
+
52
+ def parse_stream_chunk(chunk: Any) -> list[StreamEvent]:
53
+ if not isinstance(chunk, dict):
54
+ return []
55
+
56
+ events: list[StreamEvent] = []
57
+ for label, payload in chunk.items():
58
+ events.extend(_parse_payload_events(label, payload))
59
+ return events
60
+
61
+
62
+ def extract_text_response(result: Any) -> str:
63
+ if result is None:
64
+ return ""
65
+
66
+ if isinstance(result, str):
67
+ return result
68
+
69
+ if isinstance(result, dict):
70
+ messages = result.get("messages")
71
+ if isinstance(messages, list):
72
+ for message in reversed(messages):
73
+ text = _extract_message_text(message)
74
+ if text:
75
+ return text
76
+ for key in ("output", "result", "response"):
77
+ value = result.get(key)
78
+ if isinstance(value, str) and value.strip():
79
+ return value
80
+ return repr(result)
81
+
82
+ return _extract_message_text(result) or repr(result)
83
+
84
+
85
+ def _summarize_payload(payload: Any) -> str:
86
+ if payload is None:
87
+ return ""
88
+ if isinstance(payload, dict):
89
+ filtered = {key: value for key, value in payload.items() if key not in {"messages", "skills_metadata"}}
90
+ if not filtered:
91
+ return ""
92
+ return _compact_text(filtered)
93
+ return _compact_text(payload)
94
+
95
+
96
+ def _normalize_trace_label(label: str) -> str:
97
+ cleaned = label.replace("Middleware", "").replace("_", " ").strip()
98
+ parts = [part.strip() for part in cleaned.split(".") if part.strip()]
99
+ normalized = " / ".join(parts) if parts else cleaned
100
+ return normalized.lower()
101
+
102
+
103
+ def _normalize_role(role: str | None) -> str | None:
104
+ if role is None:
105
+ return None
106
+ normalized = role.strip().lower()
107
+ if normalized in {"ai", "assistant"}:
108
+ return "assistant"
109
+ if normalized in {"human", "user"}:
110
+ return "user"
111
+ if normalized == "system":
112
+ return "system"
113
+ return None
114
+
115
+
116
+ def _parse_payload_events(label: str, payload: Any) -> list[StreamEvent]:
117
+ if not isinstance(payload, dict):
118
+ summarized = _compact_text(payload)
119
+ if not summarized:
120
+ return []
121
+ return [StreamEvent(kind="state", label=_normalize_trace_label(label), text=summarized)]
122
+
123
+ events: list[StreamEvent] = []
124
+ messages = payload.get("messages", [])
125
+ if isinstance(messages, list):
126
+ for message in messages:
127
+ events.extend(_parse_message_event(label, message))
128
+
129
+ summary = _summarize_payload(payload)
130
+ if summary:
131
+ events.append(StreamEvent(kind="state", label=_normalize_trace_label(label), text=summary))
132
+ return events
133
+
134
+
135
+ def _parse_message_event(label: str, message: Any) -> list[StreamEvent]:
136
+ role = _message_role(message)
137
+ events: list[StreamEvent] = []
138
+
139
+ if role == "assistant":
140
+ for tool_call in _extract_tool_calls(message):
141
+ tool_name = tool_call.get("name") or _normalize_trace_label(label)
142
+ tool_input = tool_call.get("args", tool_call.get("arguments"))
143
+ trace_id = str(tool_call.get("id", "")).strip()
144
+ if str(tool_name).strip() == "task" and isinstance(tool_input, dict):
145
+ subagent_type = str(tool_input.get("subagent_type", "")).strip()
146
+ description = _compact_text(tool_input.get("description")) or _compact_text(tool_input) or "invoked"
147
+ if subagent_type:
148
+ events.append(
149
+ StreamEvent(
150
+ kind="subagent",
151
+ label=subagent_type,
152
+ text=description,
153
+ phase="start",
154
+ trace_id=trace_id,
155
+ )
156
+ )
157
+ continue
158
+ events.append(
159
+ StreamEvent(
160
+ kind="tool",
161
+ label=str(tool_name),
162
+ text=_compact_text(tool_input) or "invoked",
163
+ phase="start",
164
+ trace_id=trace_id,
165
+ )
166
+ )
167
+ text = _extract_message_text(message)
168
+ if text:
169
+ events.append(StreamEvent(kind="assistant", label="assistant", text=text, phase="message"))
170
+ return events
171
+
172
+ if role == "tool":
173
+ tool_name = _message_name(message) or _normalize_trace_label(label)
174
+ tool_text = _extract_message_text(message)
175
+ trace_id = _message_tool_call_id(message)
176
+ if tool_name == "task":
177
+ events.append(
178
+ StreamEvent(
179
+ kind="subagent",
180
+ label=tool_name,
181
+ text=tool_text or "(ok)",
182
+ phase="end",
183
+ trace_id=trace_id,
184
+ )
185
+ )
186
+ return events
187
+ events.append(
188
+ StreamEvent(
189
+ kind="tool",
190
+ label=tool_name,
191
+ text=tool_text or "(ok)",
192
+ phase="end",
193
+ trace_id=trace_id,
194
+ )
195
+ )
196
+ return events
197
+
198
+ text = _extract_message_text(message)
199
+ if text:
200
+ events.append(StreamEvent(kind="state", label=_normalize_trace_label(label), text=text, phase="update"))
201
+ return events
202
+
203
+
204
+ def _message_role(message: Any) -> str | None:
205
+ role = getattr(message, "type", None)
206
+ if role is None and isinstance(message, dict):
207
+ role = message.get("type") or message.get("role")
208
+ return _normalize_role(role) or ("tool" if str(role).strip().lower() == "tool" else None)
209
+
210
+
211
+ def _message_name(message: Any) -> str:
212
+ name = getattr(message, "name", None)
213
+ if name is None and isinstance(message, dict):
214
+ name = message.get("name")
215
+ return str(name).strip() if name else ""
216
+
217
+
218
+ def _message_tool_call_id(message: Any) -> str:
219
+ tool_call_id = getattr(message, "tool_call_id", None)
220
+ if tool_call_id is None and isinstance(message, dict):
221
+ tool_call_id = message.get("tool_call_id")
222
+ return str(tool_call_id).strip() if tool_call_id else ""
223
+
224
+
225
+ def _extract_tool_calls(message: Any) -> list[dict[str, Any]]:
226
+ tool_calls = getattr(message, "tool_calls", None)
227
+ if tool_calls is None and isinstance(message, dict):
228
+ tool_calls = message.get("tool_calls")
229
+ if not isinstance(tool_calls, list):
230
+ return []
231
+ return [call for call in tool_calls if isinstance(call, dict)]
232
+
233
+
234
+ def _extract_message_text(message: Any) -> str:
235
+ content = getattr(message, "content", None)
236
+ if content is None and isinstance(message, dict):
237
+ content = message.get("content")
238
+ if isinstance(content, str):
239
+ return content.strip()
240
+ if isinstance(content, list):
241
+ parts: list[str] = []
242
+ for item in content:
243
+ if isinstance(item, str):
244
+ parts.append(item)
245
+ elif isinstance(item, dict):
246
+ text = item.get("text")
247
+ if isinstance(text, str):
248
+ parts.append(text)
249
+ return "\n".join(part for part in parts if part).strip()
250
+ return ""
251
+
252
+
253
+ def _compact_text(value: Any, limit: int = TRACE_PREVIEW_LIMIT) -> str:
254
+ if value is None:
255
+ return ""
256
+ if isinstance(value, str):
257
+ text = value.strip()
258
+ else:
259
+ try:
260
+ text = json.dumps(value, ensure_ascii=True, sort_keys=True)
261
+ except TypeError:
262
+ text = repr(value)
263
+ compacted = " ".join(text.split())
264
+ if len(compacted) <= limit:
265
+ return compacted
266
+ return f"{compacted[: limit - 3]}..."