pcode 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.
- pcode/__init__.py +7 -0
- pcode/_job_supervisor.py +65 -0
- pcode/agent.py +592 -0
- pcode/anthropic_oauth.py +469 -0
- pcode/app.py +3165 -0
- pcode/appearance.py +181 -0
- pcode/aside.py +541 -0
- pcode/aside_guard.py +39 -0
- pcode/aside_ui.py +683 -0
- pcode/auth.py +113 -0
- pcode/block.py +39 -0
- pcode/browser.py +393 -0
- pcode/cache_diagnostics.py +336 -0
- pcode/cache_settings.py +58 -0
- pcode/cache_warnings.py +139 -0
- pcode/claude_sdk/__init__.py +96 -0
- pcode/claude_sdk/cli.py +52 -0
- pcode/claude_sdk/errors.py +61 -0
- pcode/claude_sdk/messages.py +154 -0
- pcode/claude_sdk/model.py +179 -0
- pcode/claude_sdk/resume.py +114 -0
- pcode/claude_sdk/session.py +547 -0
- pcode/claude_sdk/session_pool.py +251 -0
- pcode/claude_sdk/workspace.py +34 -0
- pcode/cli.py +73 -0
- pcode/clipboard.py +76 -0
- pcode/code_mode.py +43 -0
- pcode/codex_login.py +189 -0
- pcode/command_transcript.py +71 -0
- pcode/commands.py +145 -0
- pcode/compaction.py +457 -0
- pcode/completion.py +263 -0
- pcode/config.py +223 -0
- pcode/context_breakdown.py +178 -0
- pcode/context_usage.py +52 -0
- pcode/controller.py +3496 -0
- pcode/conversation_ids.py +27 -0
- pcode/conversation_tree.py +160 -0
- pcode/copy_ui.py +172 -0
- pcode/delegation.py +236 -0
- pcode/delta.py +295 -0
- pcode/diagnostics.py +226 -0
- pcode/edit_preview.py +158 -0
- pcode/edit_transcript.py +287 -0
- pcode/edit_ui.py +850 -0
- pcode/edits.py +162 -0
- pcode/email_remote/__init__.py +14 -0
- pcode/email_remote/cli.py +133 -0
- pcode/email_remote/listener.py +695 -0
- pcode/email_remote/mailbox.py +336 -0
- pcode/email_remote/outbound.py +75 -0
- pcode/email_remote/parsing.py +226 -0
- pcode/email_remote/render.py +175 -0
- pcode/email_remote/state.py +79 -0
- pcode/error_report.py +38 -0
- pcode/ext.py +602 -0
- pcode/extension_guide.md +311 -0
- pcode/extensions/browser.py +284 -0
- pcode/extensions/ponytail.py +217 -0
- pcode/extensions/sandbox.py +138 -0
- pcode/extensions/session_history.py +170 -0
- pcode/extensions/web_research.py +89 -0
- pcode/file_refs.py +265 -0
- pcode/filesystem.py +138 -0
- pcode/frame.py +149 -0
- pcode/gateway.py +465 -0
- pcode/git_diff.py +497 -0
- pcode/history.py +589 -0
- pcode/history_cursor.py +147 -0
- pcode/history_embeddings.py +102 -0
- pcode/host.py +941 -0
- pcode/host_protocol.py +291 -0
- pcode/host_ui.py +225 -0
- pcode/input_keys.py +35 -0
- pcode/inspection.py +289 -0
- pcode/inspector_ui.py +448 -0
- pcode/isolated_delegation.py +425 -0
- pcode/job_notices.py +54 -0
- pcode/jobs.py +584 -0
- pcode/jobs_ui.py +265 -0
- pcode/keymap.py +215 -0
- pcode/layout_speed.py +103 -0
- pcode/links.py +90 -0
- pcode/links_ui.py +200 -0
- pcode/live.py +1301 -0
- pcode/llm_proxy.py +44 -0
- pcode/logins.py +233 -0
- pcode/mcp.py +683 -0
- pcode/mcp_notice.py +91 -0
- pcode/mcp_oauth.py +381 -0
- pcode/meridian.py +233 -0
- pcode/meridian_process.py +337 -0
- pcode/meridian_reminders.py +107 -0
- pcode/meridian_setup.py +356 -0
- pcode/model_metadata.py +451 -0
- pcode/model_ui.py +168 -0
- pcode/models.py +209 -0
- pcode/native_results.py +119 -0
- pcode/oauth_pages.py +66 -0
- pcode/output_limits.py +41 -0
- pcode/paste.py +45 -0
- pcode/path_completion.py +66 -0
- pcode/plan_preview.py +131 -0
- pcode/plan_sizes.py +35 -0
- pcode/planning.py +161 -0
- pcode/popup_ui.py +873 -0
- pcode/preferences.py +1176 -0
- pcode/prefix_keys.py +490 -0
- pcode/profile_benchmark.py +332 -0
- pcode/profiling.py +566 -0
- pcode/project_trust.py +94 -0
- pcode/prompt_keys.py +286 -0
- pcode/remote.py +717 -0
- pcode/remote_print.py +125 -0
- pcode/remote_profile.py +237 -0
- pcode/replay_benchmark.py +319 -0
- pcode/repo_context.py +158 -0
- pcode/retries.py +28 -0
- pcode/rpc.py +280 -0
- pcode/runtime.py +244 -0
- pcode/sandbox.py +454 -0
- pcode/session_naming.py +82 -0
- pcode/session_ui.py +550 -0
- pcode/sessions.py +892 -0
- pcode/shell.py +113 -0
- pcode/shell_mode.py +172 -0
- pcode/shell_tools.py +459 -0
- pcode/skills.py +161 -0
- pcode/stall_log.py +187 -0
- pcode/startup_input.py +96 -0
- pcode/steering.py +27 -0
- pcode/stream_display.py +188 -0
- pcode/stream_events.py +437 -0
- pcode/strict_tools.py +130 -0
- pcode/syntax.py +43 -0
- pcode/syntax_colors.py +206 -0
- pcode/task_prompt.py +28 -0
- pcode/task_worktrees.py +482 -0
- pcode/terminal_notify.py +383 -0
- pcode/theme.py +135 -0
- pcode/theme_gallery.py +88 -0
- pcode/thinking_markdown.py +41 -0
- pcode/token_accounting.py +67 -0
- pcode/tool_display.py +784 -0
- pcode/tool_output_limits.py +108 -0
- pcode/tool_panel.py +377 -0
- pcode/transcript_log.py +127 -0
- pcode/transcript_notice.py +103 -0
- pcode/tree_ui.py +288 -0
- pcode/turn.py +44 -0
- pcode/ui.py +3745 -0
- pcode/usage.py +293 -0
- pcode/word_wrap.py +107 -0
- pcode/worker_ui.py +235 -0
- pcode/workers.py +136 -0
- pcode/workspace.py +57 -0
- pcode/workspace_filesystem.py +72 -0
- pcode/worktree.py +650 -0
- pcode-0.1.0.dist-info/METADATA +145 -0
- pcode-0.1.0.dist-info/RECORD +163 -0
- pcode-0.1.0.dist-info/WHEEL +4 -0
- pcode-0.1.0.dist-info/entry_points.txt +3 -0
- pcode-0.1.0.dist-info/licenses/LICENSE +21 -0
pcode/workers.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Each delegated worker's own stream, kept for the read-only viewer; no terminal imports.
|
|
2
|
+
|
|
3
|
+
The conversation's transcript shows a worker only as its delegate row and the
|
|
4
|
+
calls beneath it. This keeps what the worker itself said and did, in order, so
|
|
5
|
+
`/agents` can show it while it runs and after it settles. Memory only: nothing
|
|
6
|
+
here is journaled, so a resumed session starts with no workers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from time import monotonic
|
|
11
|
+
|
|
12
|
+
from pcode.runtime import ChildPlan, ChildText, ToolStarted, ToolSummary
|
|
13
|
+
from pcode.tool_display import PLAN_TOOLS
|
|
14
|
+
|
|
15
|
+
DELEGATE = "delegate_task"
|
|
16
|
+
# Workers accumulate over a long session; keep the recent ones.
|
|
17
|
+
WORKER_HISTORY = 20
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class WorkerEntry:
|
|
22
|
+
"""One step of a worker's stream: a run of prose or reasoning, or one tool call."""
|
|
23
|
+
|
|
24
|
+
kind: str # "text", "thinking" or "tool"
|
|
25
|
+
text: str = ""
|
|
26
|
+
tool: ToolStarted | ToolSummary | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Worker:
|
|
31
|
+
call_id: str
|
|
32
|
+
agent: str
|
|
33
|
+
task: str
|
|
34
|
+
started: float = field(default_factory=monotonic)
|
|
35
|
+
settled: float | None = None
|
|
36
|
+
failed: bool = False
|
|
37
|
+
# Why it stopped when that was not a normal finish, e.g. "Interrupted".
|
|
38
|
+
ending: str = ""
|
|
39
|
+
activity: str = ""
|
|
40
|
+
plan: list[dict] = field(default_factory=list)
|
|
41
|
+
entries: list[WorkerEntry] = field(default_factory=list)
|
|
42
|
+
# Bumped on every change, so a viewer can skip unchanged repaints.
|
|
43
|
+
version: int = 0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def running(self) -> bool:
|
|
47
|
+
return self.settled is None
|
|
48
|
+
|
|
49
|
+
def elapsed(self) -> float:
|
|
50
|
+
return (self.settled if self.settled is not None else monotonic()) - self.started
|
|
51
|
+
|
|
52
|
+
def state(self) -> str:
|
|
53
|
+
if self.running:
|
|
54
|
+
return self.activity or "Starting"
|
|
55
|
+
return self.ending or ("Failed" if self.failed else "Done")
|
|
56
|
+
|
|
57
|
+
def add_text(self, event: ChildText) -> None:
|
|
58
|
+
kind = "thinking" if event.thinking else "text"
|
|
59
|
+
last = self.entries[-1] if self.entries else None
|
|
60
|
+
if event.start or last is None or last.kind != kind:
|
|
61
|
+
self.entries.append(WorkerEntry(kind, event.text))
|
|
62
|
+
else:
|
|
63
|
+
last.text += event.text
|
|
64
|
+
|
|
65
|
+
def add_tool(self, event: ToolStarted | ToolSummary) -> None:
|
|
66
|
+
"""A start adds a row; its restatement or result replaces that row."""
|
|
67
|
+
for entry in reversed(self.entries):
|
|
68
|
+
if entry.tool is not None and entry.tool.call_id == event.call_id:
|
|
69
|
+
entry.tool = event
|
|
70
|
+
return
|
|
71
|
+
self.entries.append(WorkerEntry("tool", tool=event))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Workers:
|
|
76
|
+
items: list[Worker] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
def get(self, call_id: str) -> Worker | None:
|
|
79
|
+
return next((w for w in self.items if w.call_id == call_id), None)
|
|
80
|
+
|
|
81
|
+
def record(self, event) -> None:
|
|
82
|
+
"""Fold one runtime event into the worker it belongs to, if any."""
|
|
83
|
+
if isinstance(event, ToolStarted | ToolSummary) and event.name == DELEGATE:
|
|
84
|
+
self._delegate(event)
|
|
85
|
+
return
|
|
86
|
+
call_id = (
|
|
87
|
+
event.call_id
|
|
88
|
+
if isinstance(event, ChildText | ChildPlan)
|
|
89
|
+
else event.parent_call_id
|
|
90
|
+
if isinstance(event, ToolStarted | ToolSummary)
|
|
91
|
+
else ""
|
|
92
|
+
)
|
|
93
|
+
worker = self.get(call_id) if call_id else None
|
|
94
|
+
if worker is None:
|
|
95
|
+
return
|
|
96
|
+
if isinstance(event, ChildText):
|
|
97
|
+
worker.add_text(event)
|
|
98
|
+
elif isinstance(event, ChildPlan):
|
|
99
|
+
worker.plan = event.items
|
|
100
|
+
elif event.name not in PLAN_TOOLS:
|
|
101
|
+
# The plan has its own section; its tool calls would only repeat it.
|
|
102
|
+
worker.add_tool(event)
|
|
103
|
+
worker.version += 1
|
|
104
|
+
|
|
105
|
+
def _delegate(self, event: ToolStarted | ToolSummary) -> None:
|
|
106
|
+
worker = self.get(event.call_id)
|
|
107
|
+
if isinstance(event, ToolStarted):
|
|
108
|
+
if worker is None:
|
|
109
|
+
worker = Worker(event.call_id, event.agent or "worker", event.task)
|
|
110
|
+
self.items.append(worker)
|
|
111
|
+
del self.items[:-WORKER_HISTORY]
|
|
112
|
+
# A restated start carries the worker's current phase.
|
|
113
|
+
worker.activity = event.activity
|
|
114
|
+
elif worker is not None and worker.running:
|
|
115
|
+
worker.settled = monotonic()
|
|
116
|
+
worker.failed = event.failed
|
|
117
|
+
else:
|
|
118
|
+
return
|
|
119
|
+
worker.version += 1
|
|
120
|
+
|
|
121
|
+
def end_turn(self) -> None:
|
|
122
|
+
"""A turn that ends under a running worker stopped it, e.g. by cancellation."""
|
|
123
|
+
for worker in self.items:
|
|
124
|
+
if worker.running:
|
|
125
|
+
worker.settled = monotonic()
|
|
126
|
+
worker.failed = True
|
|
127
|
+
worker.ending = "Interrupted"
|
|
128
|
+
worker.version += 1
|
|
129
|
+
|
|
130
|
+
def running(self) -> int:
|
|
131
|
+
return sum(worker.running for worker in self.items)
|
|
132
|
+
|
|
133
|
+
def latest(self) -> Worker | None:
|
|
134
|
+
"""The newest running worker, else the newest one."""
|
|
135
|
+
running = [worker for worker in self.items if worker.running]
|
|
136
|
+
return (running or self.items or [None])[-1]
|
pcode/workspace.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""A workspace can be deleted while a session is still working in it.
|
|
2
|
+
|
|
3
|
+
Session worktrees are removed from outside the session that owns them: another
|
|
4
|
+
session merges and removes it, `/worktree clean` sweeps it, someone runs `git
|
|
5
|
+
worktree prune`. Every tool in this process then resolves paths against a
|
|
6
|
+
directory that is gone.
|
|
7
|
+
|
|
8
|
+
Harness reports that as a retryable failure, on the assumption that the model
|
|
9
|
+
destroyed its own working directory and can `cd` out of it. Here the working
|
|
10
|
+
directory is the workspace, fixed for the whole run, so no command the model
|
|
11
|
+
can write will succeed -- a `cd /tmp` prefix does not help, because the tool
|
|
12
|
+
chdirs into the workspace before running it. Retrying only spends the tool
|
|
13
|
+
budget and ends the turn with a message blaming the model. Stop on the first
|
|
14
|
+
call instead, and name the directory that disappeared.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pydantic_ai.capabilities import AbstractCapability
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkspaceGoneError(Exception):
|
|
23
|
+
"""The workspace directory disappeared; nothing in the run can recover.
|
|
24
|
+
|
|
25
|
+
Deliberately not `ModelRetry`: the model has no move that fixes it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def require_workspace(workspace: Path) -> None:
|
|
30
|
+
"""Raise `WorkspaceGoneError` unless tools can still run in `workspace`."""
|
|
31
|
+
if workspace.is_dir():
|
|
32
|
+
return
|
|
33
|
+
raise WorkspaceGoneError(
|
|
34
|
+
f"The workspace {workspace} no longer exists, so no tool can run in it. "
|
|
35
|
+
"It was deleted while this session was open, usually by a worktree removed "
|
|
36
|
+
"from elsewhere. Resume this conversation in another directory with "
|
|
37
|
+
"`pcode -C DIR --continue <session>`."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WorkspaceGuard(AbstractCapability):
|
|
42
|
+
"""Check the workspace still exists before a run and before each tool call.
|
|
43
|
+
|
|
44
|
+
The run-start check comes first in the capability list, so it names the
|
|
45
|
+
missing directory before Harness's own capabilities fail on it with a
|
|
46
|
+
generic `WorkspaceUnavailableError`.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, workspace: Path) -> None:
|
|
50
|
+
self.workspace = Path(workspace)
|
|
51
|
+
|
|
52
|
+
async def before_run(self, ctx) -> None:
|
|
53
|
+
require_workspace(self.workspace)
|
|
54
|
+
|
|
55
|
+
async def wrap_tool_execute(self, ctx, *, call, tool_def, args, handler):
|
|
56
|
+
require_workspace(self.workspace)
|
|
57
|
+
return await handler(args)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Unconfined file paths with a stable workspace-relative base.
|
|
2
|
+
|
|
3
|
+
Harness resolves relative paths from the workspace's working directory and
|
|
4
|
+
reports walked paths relative to it; a `root_dir` of `/` lifts the boundary,
|
|
5
|
+
so the workspace stays the base without being a sandbox. See
|
|
6
|
+
dev/dependencies.md before upgrading the filesystem integration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import fields
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from pydantic_ai_harness.filesystem import FileSystem, FileSystemToolset
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _any_depth(pattern: str) -> str:
|
|
16
|
+
"""Match a protected-file pattern at any depth, since the root is `/`."""
|
|
17
|
+
return pattern if pattern.startswith("**/") else f"**/{pattern}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkspaceFileSystem(FileSystem):
|
|
21
|
+
"""Keep workspace-relative paths without using the workspace as a boundary."""
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
super().__post_init__()
|
|
25
|
+
# Workspace paths are POSIX, so `/` is the whole filesystem on any host.
|
|
26
|
+
self.root_dir = "/"
|
|
27
|
+
# Protected-file rules still apply, including to paths outside the workspace.
|
|
28
|
+
self.read_only_patterns = [_any_depth(pattern) for pattern in self.read_only_patterns]
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_filesystem(cls, filesystem: FileSystem) -> "WorkspaceFileSystem":
|
|
32
|
+
values = {f.name: getattr(filesystem, f.name) for f in fields(FileSystem) if f.init}
|
|
33
|
+
# Deprecated upstream aliases; passing them only re-triggers the warning.
|
|
34
|
+
values.pop("cwd", None)
|
|
35
|
+
values.pop("protected_patterns", None)
|
|
36
|
+
return cls(**values)
|
|
37
|
+
|
|
38
|
+
def get_instructions(self):
|
|
39
|
+
async def instructions(ctx) -> str:
|
|
40
|
+
base = await ctx.workspace.working_dir()
|
|
41
|
+
return (
|
|
42
|
+
f"File tools accept absolute paths anywhere on the host and relative paths "
|
|
43
|
+
f"based on the workspace {base!r}, including '..'. "
|
|
44
|
+
"Shell cd does not change that file-tool base. File tools are not a sandbox. "
|
|
45
|
+
"Search and listing tools default to the workspace. "
|
|
46
|
+
"Returned relative paths use that same workspace base. "
|
|
47
|
+
"Existing protected-file write rules still apply to file tools, not shell commands."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return instructions
|
|
51
|
+
|
|
52
|
+
def _toolset_type(self) -> type[FileSystemToolset]:
|
|
53
|
+
return FileSystemToolset
|
|
54
|
+
|
|
55
|
+
def _file_system_toolset(self) -> FileSystemToolset:
|
|
56
|
+
if self._toolset is None:
|
|
57
|
+
self._toolset = self._toolset_type()(
|
|
58
|
+
root_dir=Path(self.root_dir),
|
|
59
|
+
allowed_patterns=self.allowed_patterns,
|
|
60
|
+
denied_patterns=self.denied_patterns,
|
|
61
|
+
read_only_patterns=self.read_only_patterns,
|
|
62
|
+
max_read_lines=self.max_read_lines,
|
|
63
|
+
max_read_chars=self.max_read_chars,
|
|
64
|
+
max_list_results=self.max_list_results,
|
|
65
|
+
max_search_results=self.max_search_results,
|
|
66
|
+
max_find_results=self.max_find_results,
|
|
67
|
+
id=self.id or "file_system",
|
|
68
|
+
content_hashes=self.content_hashes,
|
|
69
|
+
tools=self.tools,
|
|
70
|
+
max_retries=self.max_retries,
|
|
71
|
+
)
|
|
72
|
+
return self._toolset
|