pal-code 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.
- pal/__init__.py +29 -0
- pal/__main__.py +3 -0
- pal/access_manager/__init__.py +16 -0
- pal/access_manager/components/__init__.py +14 -0
- pal/access_manager/components/access_control.py +41 -0
- pal/access_manager/components/base.py +39 -0
- pal/access_manager/components/intervention.py +31 -0
- pal/access_manager/components/privilege_registry.py +30 -0
- pal/access_manager/pipeline.py +68 -0
- pal/access_manager/utils/__init__.py +2 -0
- pal/agent_constraints/__init__.py +3 -0
- pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
- pal/agent_constraints/components/__init__.py +3 -0
- pal/agent_constraints/components/constraints.py +22 -0
- pal/agent_constraints/pipeline.py +22 -0
- pal/agent_constraints/utils/__init__.py +2 -0
- pal/agents/__init__.py +12 -0
- pal/agents/coding.py +14 -0
- pal/agents/docs.py +15 -0
- pal/chat/__init__.py +4 -0
- pal/chat/demo.py +231 -0
- pal/chat/providers.py +23 -0
- pal/chat/service.py +591 -0
- pal/chat/services/__init__.py +26 -0
- pal/chat/services/base.py +142 -0
- pal/chat/services/data.py +142 -0
- pal/chat/services/media.py +123 -0
- pal/chat/services/web.py +60 -0
- pal/chat/sessions.py +108 -0
- pal/chat/tools.py +446 -0
- pal/chat/uploads.py +81 -0
- pal/cli.py +502 -0
- pal/compression/__init__.py +12 -0
- pal/compression/components/__init__.py +64 -0
- pal/compression/components/adverbs.py +20 -0
- pal/compression/components/hedges.py +54 -0
- pal/compression/components/interjections.py +20 -0
- pal/compression/components/minifier.py +56 -0
- pal/compression/components/output_compressor.py +13 -0
- pal/compression/components/pleonasms.py +54 -0
- pal/compression/components/punctuations.py +36 -0
- pal/compression/components/stopwords.py +62 -0
- pal/compression/history.py +346 -0
- pal/compression/pipeline.py +58 -0
- pal/compression/utils/__init__.py +30 -0
- pal/compression/utils/negations.py +18 -0
- pal/compression/utils/nltk_setup.py +20 -0
- pal/compression/utils/pronouns.py +27 -0
- pal/compression/utils/proselint_setup.py +9 -0
- pal/compression/utils/spacy_setup.py +22 -0
- pal/config/default.yaml +163 -0
- pal/config.py +504 -0
- pal/context_manager/__init__.py +7 -0
- pal/context_manager/chat_context.py +117 -0
- pal/context_manager/components/__init__.py +5 -0
- pal/context_manager/components/assembler.py +75 -0
- pal/context_manager/components/context_cache.py +45 -0
- pal/context_manager/components/kv_bridge.py +16 -0
- pal/context_manager/components/pager.py +48 -0
- pal/context_manager/components/simple_context_manager.py +56 -0
- pal/context_manager/components/turns.py +88 -0
- pal/context_manager/pipeline.py +27 -0
- pal/context_manager/utils/__init__.py +18 -0
- pal/context_manager/utils/hashing.py +5 -0
- pal/context_manager/utils/relevance.py +59 -0
- pal/context_manager/utils/storage.py +37 -0
- pal/documents/__init__.py +4 -0
- pal/documents/components/__init__.py +0 -0
- pal/documents/components/chunker.py +79 -0
- pal/documents/components/index.py +155 -0
- pal/documents/utils/__init__.py +1 -0
- pal/errors.py +40 -0
- pal/input_converters/__init__.py +8 -0
- pal/input_converters/components/__init__.py +0 -0
- pal/input_converters/components/base.py +38 -0
- pal/input_converters/components/pandoc_converter.py +87 -0
- pal/input_converters/components/pdf_converter.py +46 -0
- pal/input_converters/components/pptx_converter.py +52 -0
- pal/input_converters/components/xlsx_converter.py +38 -0
- pal/input_converters/pipeline.py +72 -0
- pal/input_converters/utils/__init__.py +1 -0
- pal/input_converters/workspace.py +56 -0
- pal/input_savers/INPUT_RULES.md +2 -0
- pal/input_savers/__init__.py +5 -0
- pal/input_savers/components/__init__.py +0 -0
- pal/input_savers/components/base.py +40 -0
- pal/input_savers/components/methods.py +41 -0
- pal/input_savers/components/verifier.py +39 -0
- pal/input_savers/pipeline.py +154 -0
- pal/input_savers/utils/__init__.py +0 -0
- pal/input_savers/utils/guards.py +94 -0
- pal/knowledge_graph/__init__.py +10 -0
- pal/knowledge_graph/components/__init__.py +0 -0
- pal/knowledge_graph/components/base.py +50 -0
- pal/knowledge_graph/components/builder.py +54 -0
- pal/knowledge_graph/components/retriever.py +100 -0
- pal/knowledge_graph/components/workspace.py +116 -0
- pal/knowledge_graph/pipeline.py +215 -0
- pal/knowledge_graph/utils/__init__.py +1 -0
- pal/latency_tracker/__init__.py +3 -0
- pal/latency_tracker/pipeline.py +86 -0
- pal/llm_scheduler/__init__.py +11 -0
- pal/llm_scheduler/api.py +137 -0
- pal/llm_scheduler/code.py +28 -0
- pal/llm_scheduler/core.py +92 -0
- pal/llm_scheduler/local.py +215 -0
- pal/llm_scheduler/pipeline.py +42 -0
- pal/llm_scheduler/types.py +92 -0
- pal/llm_wrapper/__init__.py +34 -0
- pal/llm_wrapper/adapter.py +316 -0
- pal/llm_wrapper/components/__init__.py +23 -0
- pal/llm_wrapper/components/adapter_types.py +90 -0
- pal/llm_wrapper/components/base.py +195 -0
- pal/llm_wrapper/components/features.py +104 -0
- pal/llm_wrapper/components/litellm_wrapper.py +215 -0
- pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
- pal/llm_wrapper/modes.py +73 -0
- pal/llm_wrapper/pipeline.py +61 -0
- pal/llm_wrapper/utils/__init__.py +4 -0
- pal/llm_wrapper/utils/registry.py +49 -0
- pal/llm_wrapper/utils/token_counting.py +10 -0
- pal/local_llm/__init__.py +5 -0
- pal/local_llm/components/__init__.py +0 -0
- pal/local_llm/components/huggingface_local.py +217 -0
- pal/local_llm/components/local_llm.py +60 -0
- pal/logging_setup.py +18 -0
- pal/memory/__init__.py +7 -0
- pal/memory/components/__init__.py +6 -0
- pal/memory/components/lru_k.py +56 -0
- pal/memory/components/manager.py +98 -0
- pal/memory/components/note.py +54 -0
- pal/memory/components/storage_manager.py +60 -0
- pal/memory/pipeline.py +122 -0
- pal/memory/utils/__init__.py +3 -0
- pal/memory/utils/keyword_overlap.py +3 -0
- pal/memory_tracker/__init__.py +3 -0
- pal/memory_tracker/components/__init__.py +0 -0
- pal/memory_tracker/components/process.py +56 -0
- pal/memory_tracker/pipeline.py +44 -0
- pal/metrics/__init__.py +4 -0
- pal/metrics/components/__init__.py +4 -0
- pal/metrics/components/collector.py +140 -0
- pal/metrics/components/exporter.py +28 -0
- pal/metrics/pipeline.py +49 -0
- pal/metrics/utils/__init__.py +2 -0
- pal/notes_manager/__init__.py +5 -0
- pal/notes_manager/components/__init__.py +0 -0
- pal/notes_manager/components/privacy.py +14 -0
- pal/notes_manager/components/recall.py +31 -0
- pal/notes_manager/components/tidy.py +28 -0
- pal/notes_manager/pipeline.py +138 -0
- pal/orchestrator/__init__.py +26 -0
- pal/orchestrator/components/__init__.py +26 -0
- pal/orchestrator/components/agent.py +105 -0
- pal/orchestrator/components/cgroup_manager.py +209 -0
- pal/orchestrator/components/context_interrupt.py +220 -0
- pal/orchestrator/components/limits.py +9 -0
- pal/orchestrator/components/scheduler.py +69 -0
- pal/orchestrator/components/syscall_scheduler.py +200 -0
- pal/orchestrator/pipeline.py +228 -0
- pal/orchestrator/utils/__init__.py +4 -0
- pal/orchestrator/utils/device.py +164 -0
- pal/orchestrator/utils/protocol.py +32 -0
- pal/os_kernel/__init__.py +5 -0
- pal/os_kernel/filesystem.py +80 -0
- pal/os_kernel/network.py +146 -0
- pal/os_kernel/pipeline.py +175 -0
- pal/os_kernel/sysinfo.py +36 -0
- pal/paths.py +59 -0
- pal/pipeline.py +177 -0
- pal/py.typed +0 -0
- pal/redact.py +19 -0
- pal/sdk/__init__.py +33 -0
- pal/sdk/adapters.py +43 -0
- pal/sdk/agent.py +99 -0
- pal/sdk/components/__init__.py +14 -0
- pal/sdk/components/autogen_adapter.py +65 -0
- pal/sdk/components/autogen_client.py +98 -0
- pal/sdk/components/framework_adapter_base.py +69 -0
- pal/sdk/components/metagpt_adapter.py +46 -0
- pal/sdk/components/open_interpreter_adapter.py +47 -0
- pal/sdk/components/tool_catalog.py +44 -0
- pal/sdk/os_api.py +84 -0
- pal/sdk/patterns.py +129 -0
- pal/sdk/pipeline.py +240 -0
- pal/sdk/queries.py +68 -0
- pal/sdk/savers.py +88 -0
- pal/sdk/utils/__init__.py +2 -0
- pal/storage_manager/__init__.py +17 -0
- pal/storage_manager/components/__init__.py +19 -0
- pal/storage_manager/components/base.py +50 -0
- pal/storage_manager/components/local_file_backend.py +84 -0
- pal/storage_manager/components/storage_queue.py +91 -0
- pal/storage_manager/components/vector_store_backend.py +122 -0
- pal/storage_manager/pipeline.py +153 -0
- pal/storage_manager/utils/__init__.py +4 -0
- pal/storage_manager/utils/memory_adapter.py +60 -0
- pal/storage_manager/utils/sdk.py +93 -0
- pal/studio/__init__.py +0 -0
- pal/studio/builder.py +166 -0
- pal/studio/catalog.py +90 -0
- pal/studio/checks.py +129 -0
- pal/studio/clients.py +6 -0
- pal/studio/editor.py +137 -0
- pal/studio/files.py +6 -0
- pal/studio/models.py +6 -0
- pal/studio/plan.py +149 -0
- pal/studio/planner.py +205 -0
- pal/studio/policy.py +69 -0
- pal/studio/routes.py +254 -0
- pal/studio/runs.py +210 -0
- pal/task_router/__init__.py +11 -0
- pal/task_router/components/__init__.py +0 -0
- pal/task_router/components/answerer.py +37 -0
- pal/task_router/components/base.py +34 -0
- pal/task_router/components/classifier.py +40 -0
- pal/task_router/components/gates.py +23 -0
- pal/task_router/pipeline.py +61 -0
- pal/task_router/utils/__init__.py +1 -0
- pal/terminal/__init__.py +1 -0
- pal/terminal/backend.py +141 -0
- pal/terminal/bench.py +173 -0
- pal/terminal/cli.py +826 -0
- pal/terminal/clients.py +170 -0
- pal/terminal/commands.py +401 -0
- pal/terminal/decide.py +242 -0
- pal/terminal/files.py +55 -0
- pal/terminal/flow_cli.py +439 -0
- pal/terminal/hardware.py +199 -0
- pal/terminal/history.py +88 -0
- pal/terminal/keys.py +83 -0
- pal/terminal/local_models.py +261 -0
- pal/terminal/logo.py +117 -0
- pal/terminal/models.py +99 -0
- pal/terminal/policy.py +54 -0
- pal/terminal/project.py +413 -0
- pal/terminal/review.py +65 -0
- pal/terminal/security.py +239 -0
- pal/terminal/session.py +503 -0
- pal/terminal/setup.py +354 -0
- pal/terminal/syntax.py +98 -0
- pal/terminal/theme.py +37 -0
- pal/terminal/ui.py +647 -0
- pal/terminal/voice.py +137 -0
- pal/terminal/workflow.py +624 -0
- pal/token_tracker/__init__.py +3 -0
- pal/token_tracker/pipeline.py +54 -0
- pal/tokens.py +21 -0
- pal/tool_catalog.py +107 -0
- pal/tool_manager/__init__.py +17 -0
- pal/tool_manager/builtin.py +82 -0
- pal/tool_manager/components/__init__.py +19 -0
- pal/tool_manager/components/base.py +53 -0
- pal/tool_manager/components/conflict_resolver.py +27 -0
- pal/tool_manager/components/param_validator.py +41 -0
- pal/tool_manager/components/tool_call_queue.py +111 -0
- pal/tool_manager/components/tool_registry.py +60 -0
- pal/tool_manager/pipeline.py +80 -0
- pal/tool_manager/utils/__init__.py +3 -0
- pal/web/__init__.py +3 -0
- pal/web/app.py +276 -0
- pal/web/static/editor.js +219 -0
- pal/web/static/fonts/LICENSE-Inter.txt +3 -0
- pal/web/static/fonts/inter-400.woff2 +0 -0
- pal/web/static/fonts/inter-500.woff2 +0 -0
- pal/web/static/fonts/inter-600.woff2 +0 -0
- pal/web/static/fonts/inter-700.woff2 +0 -0
- pal/web/static/inter.css +5 -0
- pal/web/static/pal-logo-256.png +0 -0
- pal/web/static/pal-logo-32.png +0 -0
- pal/web/static/pal-logo-64.png +0 -0
- pal/web/static/studio.css +306 -0
- pal/web/static/studio.js +533 -0
- pal/web/static/voice.js +76 -0
- pal/web/templates/index.html +368 -0
- pal/web/templates/studio.html +120 -0
- pal_code-0.1.0.dist-info/METADATA +379 -0
- pal_code-0.1.0.dist-info/RECORD +282 -0
- pal_code-0.1.0.dist-info/WHEEL +5 -0
- pal_code-0.1.0.dist-info/entry_points.txt +2 -0
- pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- pal_code-0.1.0.dist-info/top_level.txt +1 -0
pal/terminal/clients.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""The two ways a build talks to the model: a plain chat client, and Pal. Both count every token and second the same way.
|
|
2
|
+
|
|
3
|
+
PlainClient a chat that resends the whole conversation every step (what any simple chat client does).
|
|
4
|
+
PalClient the real Pal pipeline: input savers, paged and compressed history, document excerpts, output rules.
|
|
5
|
+
Tokens are the wrapper's own counts when the model reports them, else an estimate (characters / 4), the same for both.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pal.llm_wrapper.components.base import LLMResponse, LLMSyscall, LLMWrapper, WrapperCapabilities
|
|
15
|
+
from pal.tokens import estimate_tokens
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Reply:
|
|
20
|
+
text: str
|
|
21
|
+
prompt_tokens: int = 0 # read by the main model
|
|
22
|
+
completion_tokens: int = 0 # written by the main model
|
|
23
|
+
helper_tokens: int = 0 # Pal's small jobs (trimming, checking): an extra cost, shown separately
|
|
24
|
+
seconds: float = 0.0
|
|
25
|
+
calls: int = 0
|
|
26
|
+
saved_by: dict = field(default_factory=dict) # where Pal saved tokens on this request: stage -> tokens (estimates)
|
|
27
|
+
route: str = "api" # "api" (the main model answered), "local" (the small local model did) or "tool"
|
|
28
|
+
cost_usd: float = 0.0 # what the request cost, when the provider's price is known (0 for local models)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Counts:
|
|
33
|
+
prompt: int = 0
|
|
34
|
+
completion: int = 0
|
|
35
|
+
calls: int = 0
|
|
36
|
+
seconds: float = 0.0
|
|
37
|
+
|
|
38
|
+
def since(self, before: Counts) -> Counts:
|
|
39
|
+
return Counts(self.prompt - before.prompt, self.completion - before.completion, self.calls - before.calls, self.seconds - before.seconds)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CountingWrapper(LLMWrapper):
|
|
43
|
+
"""Wraps a model and counts what goes through it. Everything else is passed on untouched."""
|
|
44
|
+
name = "counting"
|
|
45
|
+
|
|
46
|
+
def __init__(self, inner: LLMWrapper):
|
|
47
|
+
super().__init__(inner.config)
|
|
48
|
+
self.inner = inner
|
|
49
|
+
self.prompt = self.completion = self.calls = 0
|
|
50
|
+
self.estimated = 0 # calls whose token counts were estimated (the model did not report them)
|
|
51
|
+
self.seconds = 0.0
|
|
52
|
+
|
|
53
|
+
def chat(self, syscall: LLMSyscall) -> LLMResponse:
|
|
54
|
+
t0 = time.perf_counter()
|
|
55
|
+
r = self.inner.chat(syscall)
|
|
56
|
+
self.seconds += time.perf_counter() - t0
|
|
57
|
+
self.prompt += r.prompt_tokens or sum(estimate_tokens(m.get("content", "")) for m in syscall.messages)
|
|
58
|
+
self.completion += r.completion_tokens or estimate_tokens(r.text)
|
|
59
|
+
self.calls += 1
|
|
60
|
+
self.estimated += 0 if (r.prompt_tokens and r.completion_tokens) else 1
|
|
61
|
+
return r
|
|
62
|
+
|
|
63
|
+
def stream_chat(self, syscall: LLMSyscall):
|
|
64
|
+
t0 = time.perf_counter()
|
|
65
|
+
out = []
|
|
66
|
+
for piece in self.inner.stream_chat(syscall):
|
|
67
|
+
out.append(piece)
|
|
68
|
+
yield piece
|
|
69
|
+
self.seconds += time.perf_counter() - t0
|
|
70
|
+
self.prompt += sum(estimate_tokens(m.get("content", "")) for m in syscall.messages)
|
|
71
|
+
self.completion += estimate_tokens("".join(out))
|
|
72
|
+
self.calls += 1
|
|
73
|
+
|
|
74
|
+
def token_count(self, text: str) -> int:
|
|
75
|
+
return self.inner.token_count(text)
|
|
76
|
+
|
|
77
|
+
def capabilities(self) -> WrapperCapabilities:
|
|
78
|
+
return self.inner.capabilities()
|
|
79
|
+
|
|
80
|
+
def snapshot(self) -> Counts:
|
|
81
|
+
return Counts(self.prompt, self.completion, self.calls, self.seconds)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PlainClient:
|
|
85
|
+
label = "Plain chat client"
|
|
86
|
+
|
|
87
|
+
def __init__(self, core: LLMWrapper, system: str, max_tokens: int = 3000):
|
|
88
|
+
self.core, self.max_tokens = CountingWrapper(core), max_tokens
|
|
89
|
+
self.messages: list[dict] = [{"role": "system", "content": system}]
|
|
90
|
+
|
|
91
|
+
def ask(self, message: str, references: list[dict] | None = None, query: str | None = None) -> Reply:
|
|
92
|
+
if references: # a plain client has no document index: it pastes the files into the message, and so resends them every step
|
|
93
|
+
message += "\n\nReference files from the person:\n" + "\n\n".join(f"### {r['name']}\n{r['text']}" for r in references)
|
|
94
|
+
self.messages.append({"role": "user", "content": message})
|
|
95
|
+
before = self.core.snapshot()
|
|
96
|
+
text = self.core.chat(LLMSyscall(messages=list(self.messages), temperature=0.0, max_tokens=self.max_tokens)).text
|
|
97
|
+
self.messages.append({"role": "assistant", "content": text})
|
|
98
|
+
d = self.core.snapshot().since(before)
|
|
99
|
+
return Reply(text, d.prompt, d.completion, 0, d.seconds, d.calls)
|
|
100
|
+
|
|
101
|
+
def close(self) -> None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class PalClient:
|
|
106
|
+
label = "Pal"
|
|
107
|
+
|
|
108
|
+
def __init__(self, core: LLMWrapper, helper: LLMWrapper | None = None, allow_tools: frozenset | None = frozenset(), network: bool = False,
|
|
109
|
+
hot_turns: int | None = None, home: Path | None = None, model_name: str = "model", max_tokens: int = 3000, local: bool = True, project=None):
|
|
110
|
+
"""`local=False` (a computer without a GPU): no local model at all, so routing, input trimming and checking are off and the message
|
|
111
|
+
goes to the API model as typed; paging and document excerpts, which need no model, still save tokens."""
|
|
112
|
+
from pal.chat import ChatService
|
|
113
|
+
from pal.config import load_settings
|
|
114
|
+
from pal.knowledge_graph import KnowledgeGraph
|
|
115
|
+
|
|
116
|
+
self.main, self.helper = CountingWrapper(core), CountingWrapper(helper or core)
|
|
117
|
+
self.home = Path(home) if home else Path(tempfile.mkdtemp(prefix="pal-"))
|
|
118
|
+
self.allow, self.model_name, self.max_tokens = allow_tools, model_name, max_tokens
|
|
119
|
+
context = {"hot_turns": hot_turns} if hot_turns else {}
|
|
120
|
+
settings = load_settings(env={}, overrides={"storage_dir": str(self.home), "knowledge_graph": {"dir": str(self.home / "graph")}, "memory": {"enabled": False},
|
|
121
|
+
"tools": {"enabled": True, "network": bool(network)}, "context": context, "server": {"session_ttl_minutes": 240},
|
|
122
|
+
**({} if local else {"local_llm": {"enabled": False}})})
|
|
123
|
+
graph = project.graph if project is not None else KnowledgeGraph(settings.knowledge_graph)
|
|
124
|
+
self.svc = ChatService(settings, local_core=self.helper, graph=graph)
|
|
125
|
+
if project is not None: # the folder's documents and project map: passages that match each request are added
|
|
126
|
+
self.svc.preload_docs = project.documents()
|
|
127
|
+
self.role_cores: dict[str, CountingWrapper] = {} # agent -> its own model (/model plan <model>); the others use the main model
|
|
128
|
+
self.role: str | None = None # the agent making the current request
|
|
129
|
+
self.svc.api_core = lambda provider, model, api_key=None: self.core_for(self.role) # type: ignore[method-assign]
|
|
130
|
+
self.sid = "pal-" + self.home.name[-8:].replace("_", "-").lower()
|
|
131
|
+
|
|
132
|
+
def core_for(self, role: str | None) -> CountingWrapper:
|
|
133
|
+
return self.role_cores.get(role or "", self.main)
|
|
134
|
+
|
|
135
|
+
def set_role_core(self, role: str, core: LLMWrapper | None) -> None:
|
|
136
|
+
"""Give one agent its own model (None: back to the main model). The conversation stays the same."""
|
|
137
|
+
if core is None:
|
|
138
|
+
self.role_cores.pop(role, None)
|
|
139
|
+
else:
|
|
140
|
+
self.role_cores[role] = CountingWrapper(core)
|
|
141
|
+
|
|
142
|
+
def ask(self, message: str, references: list[dict] | None = None, query: str | None = None) -> Reply:
|
|
143
|
+
core = self.core_for(self.role)
|
|
144
|
+
before_main, before_helper = core.snapshot(), self.helper.snapshot()
|
|
145
|
+
attachments = [{"name": r["name"], "text": r["text"], "kind": "document"} for r in (references or [])]
|
|
146
|
+
res = self.svc.send(self.sid, "local", self.model_name, None, message, attachments, route="api", allow_tools=self.allow, query=query)
|
|
147
|
+
m, h = core.snapshot().since(before_main), self.helper.snapshot().since(before_helper)
|
|
148
|
+
return Reply(res.reply, m.prompt, m.completion, h.prompt + h.completion, m.seconds + h.seconds, m.calls, self._saved_by(), res.handled_by,
|
|
149
|
+
float(getattr(res, "cost_usd", 0.0) or 0.0))
|
|
150
|
+
|
|
151
|
+
def add_documents(self, docs: list[tuple[str, str]]) -> None:
|
|
152
|
+
"""Index documents into this conversation now (and into any later one)."""
|
|
153
|
+
self.svc.preload_docs = list(docs)
|
|
154
|
+
sess = self.svc.sessions.get_or_create(self.sid)
|
|
155
|
+
if sess.docs is not None:
|
|
156
|
+
for name, text in docs:
|
|
157
|
+
sess.docs.add(name, text)
|
|
158
|
+
|
|
159
|
+
def _saved_by(self) -> dict:
|
|
160
|
+
"""The savings the engine recorded for the turn that just ran, by stage. Every number is an estimate (characters / 4)."""
|
|
161
|
+
_, turns = self.svc.metrics.collector.snapshot()
|
|
162
|
+
if not turns:
|
|
163
|
+
return {}
|
|
164
|
+
t = turns[-1]
|
|
165
|
+
compressed = min(max(t.compression_saved, 0), max(t.saved_paging, 0)) # history savings already include what compression saved
|
|
166
|
+
found = {"input": t.saved_savers, "history": t.saved_paging - compressed, "compression": compressed, "documents": t.saved_documents, "code": t.saved_graph}
|
|
167
|
+
return {k: int(v) for k, v in found.items() if v > 0}
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
self.svc.close()
|
pal/terminal/commands.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Session commands in the style of Claude Code, for Pal: /init, /clear, /compact, /btw, /cost, /context, /memory, /add-dir, /permissions, /config,
|
|
2
|
+
/plan /build /ask, /copy, /export, /agents, /simplify, /status, /doctor, /resume, /shortcuts, /voice.
|
|
3
|
+
|
|
4
|
+
`handle_extra` returns (session, keep going) for a command it knows, or None so the caller tries the others.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
from pal import __version__
|
|
20
|
+
from pal.terminal import backend, theme, ui
|
|
21
|
+
from pal.terminal.project import NOTES_FILE
|
|
22
|
+
from pal.terminal.session import MODES, PalSession
|
|
23
|
+
from pal.tokens import estimate_tokens
|
|
24
|
+
|
|
25
|
+
COMMANDS: list[tuple[str, str]] = [
|
|
26
|
+
("/init", f"look through the project and start a {NOTES_FILE} (its instructions for Pal); you review it like any change"),
|
|
27
|
+
("/clear", "a fresh conversation (the plan, its context and pending changes stay). Alias: /reset"),
|
|
28
|
+
("/compact", "shorten the conversation to a summary to save tokens: /compact [what to keep]"),
|
|
29
|
+
("/btw", "a quick side question that is not added to the conversation: /btw what does HTTP 409 mean?"),
|
|
30
|
+
("/cost", "tokens used and saved this session, and the cost when the provider's price is known"),
|
|
31
|
+
("/context", "what goes to the model with each request, and how big each part is"),
|
|
32
|
+
("/memory", f"show the project's {NOTES_FILE}; /memory edit opens it in your editor"),
|
|
33
|
+
("/add-dir", "read files from another folder too: /add-dir ../shared-lib"),
|
|
34
|
+
("/permissions", "what Pal may do by itself (autonomy). Alias of /autonomy"),
|
|
35
|
+
("/config", "settings: model per agent, endpoint, autonomy, where things are kept. Alias: /settings"),
|
|
36
|
+
("/plan", "switch to the Plan agent, and optionally ask it: /plan add a login page"),
|
|
37
|
+
("/build", "switch to the Build agent, and optionally ask it"),
|
|
38
|
+
("/ask", "switch to the Ask agent, and optionally ask it"),
|
|
39
|
+
("/copy", "copy the last answer to the clipboard"),
|
|
40
|
+
("/export", "save the conversation as markdown: /export [file]"),
|
|
41
|
+
("/agents", "the three agents and the model each one uses"),
|
|
42
|
+
("/simplify", "review the files changed since the last commit for reuse, quality and speed; fixes go to the sandbox"),
|
|
43
|
+
("/status", "version, folder, models, plan, autonomy, this computer"),
|
|
44
|
+
("/doctor", "check this computer and the install"),
|
|
45
|
+
("/resume", "pick up an earlier plan and its conversation: /resume [name or number]"),
|
|
46
|
+
("/voice", "talk instead of typing (also ctrl+space)"),
|
|
47
|
+
("/shortcuts", "every keyboard shortcut (also ?)"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
SHORTCUTS: list[tuple[str, str]] = [
|
|
51
|
+
("enter", "send"), ("alt+enter or \\ then enter", "a new line"), ("↑ / ↓", "earlier messages (inside a long message: move between lines)"),
|
|
52
|
+
("esc esc", "clear what you typed"), ("esc", "stop Pal while it works"), ("ctrl+c", "clear the box, stop Pal, or leave on an empty box"),
|
|
53
|
+
("ctrl+d / ctrl+q", "leave"), ("tab", "switch agent: Build, Plan, Ask (or complete a command)"),
|
|
54
|
+
("shift+tab", "cycle autonomy: none, basic, plus, semi (full only with /autonomy full)"), ("/ or ctrl+p", "the command menu (↑↓ to pick, enter or tab)"),
|
|
55
|
+
("ctrl+r", "search earlier messages (again: older)"), ("ctrl+space", "talk instead of typing"), ("ctrl+l", "clear the screen"),
|
|
56
|
+
("?", "these shortcuts (on an empty box)"), ("@path", "add a file to the message"), ("drop files on the window", "attach them"),
|
|
57
|
+
("ctrl+a / ctrl+e", "start / end of the line"), ("ctrl+u / ctrl+k", "delete to the start / end of the line"), ("alt+b / alt+f", "a word back / forward"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _say(console, text: str, style: str = theme.MUTED) -> None:
|
|
62
|
+
ui.say(console, Text(text, style=style))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def copy_to_clipboard(text: str) -> str:
|
|
66
|
+
"""Copy with the system's tool, or the terminal's OSC 52 (works over SSH in most terminals). Returns how it was copied, or ''."""
|
|
67
|
+
tools = [["pbcopy"]] if sys.platform == "darwin" else [["clip"]] if os.name == "nt" else [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "-b", "-i"]]
|
|
68
|
+
for cmd in tools:
|
|
69
|
+
if shutil.which(cmd[0]):
|
|
70
|
+
try:
|
|
71
|
+
subprocess.run(cmd, input=text.encode("utf-16le" if cmd[0] == "clip" else "utf-8"), check=True, timeout=5)
|
|
72
|
+
return cmd[0]
|
|
73
|
+
except (OSError, subprocess.SubprocessError):
|
|
74
|
+
continue
|
|
75
|
+
if sys.stdout.isatty():
|
|
76
|
+
sys.stdout.write("\x1b]52;c;" + base64.b64encode(text.encode("utf-8")).decode("ascii") + "\x07")
|
|
77
|
+
sys.stdout.flush()
|
|
78
|
+
return "the terminal (OSC 52)"
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def conversation_markdown(wf) -> str:
|
|
83
|
+
b = wf.plan.b
|
|
84
|
+
out = [f"# Pal conversation: plan {wf.plan.name} ({wf.plan.branch})", "", f"_{time.strftime('%Y-%m-%d %H:%M')} · {wf.session.cwd}_", ""]
|
|
85
|
+
for m in b.convo:
|
|
86
|
+
who = "**You**" if m.get("role") == "you" else f"**Pal ({m.get('agent', '')})**"
|
|
87
|
+
out += [f"{who}", "", m.get("text", ""), ""]
|
|
88
|
+
if b.pending:
|
|
89
|
+
out += ["## Pending changes", ""] + [f"- {p}" for p in sorted(b.pending)] + [""]
|
|
90
|
+
return "\n".join(out)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def handle_extra(cmd: str, rest: str, args, session: PalSession, console, core_factory, box: dict | None):
|
|
94
|
+
wf = box["wf"] if box else None
|
|
95
|
+
host = getattr(args, "host", None) or backend.DEFAULT_HOST
|
|
96
|
+
if cmd in ("/shortcuts", "/keys", "?"):
|
|
97
|
+
grid = Table.grid(padding=(0, 3))
|
|
98
|
+
for k, what in SHORTCUTS:
|
|
99
|
+
grid.add_row(Text(k, style=f"bold {theme.WHITE}"), Text(what, style=theme.MUTED))
|
|
100
|
+
ui.say(console, grid)
|
|
101
|
+
console.print()
|
|
102
|
+
return session, True
|
|
103
|
+
|
|
104
|
+
if cmd in ("/plan", "/build", "/ask"):
|
|
105
|
+
session.mode = cmd[1:]
|
|
106
|
+
backend.save_prefs(mode=session.mode)
|
|
107
|
+
_say(console, f"Agent: {session.mode.capitalize()}")
|
|
108
|
+
console.print()
|
|
109
|
+
if rest and wf is not None:
|
|
110
|
+
from pal.terminal import flow_cli
|
|
111
|
+
flow_cli.run_turn(wf, rest, console)
|
|
112
|
+
return session, True
|
|
113
|
+
|
|
114
|
+
if cmd in ("/clear", "/reset"):
|
|
115
|
+
if wf is None or box is None:
|
|
116
|
+
return session, True
|
|
117
|
+
from pal.terminal.cli import _bind_status, _fresh
|
|
118
|
+
from pal.terminal.workflow import Workflow
|
|
119
|
+
fresh = _fresh(session, args, core_factory)
|
|
120
|
+
wf.plan.b.convo = []
|
|
121
|
+
wf.save()
|
|
122
|
+
box["wf"] = Workflow(fresh, wf.store, plan=wf.plan.name)
|
|
123
|
+
_bind_status(box["wf"])
|
|
124
|
+
_say(console, "Fresh conversation. The plan, its context and pending changes are still here (/current).")
|
|
125
|
+
console.print()
|
|
126
|
+
return fresh, True
|
|
127
|
+
|
|
128
|
+
if cmd == "/compact":
|
|
129
|
+
if wf is None or box is None or not wf.plan.b.convo:
|
|
130
|
+
_say(console, "Nothing to compact yet.")
|
|
131
|
+
console.print()
|
|
132
|
+
return session, True
|
|
133
|
+
from pal.terminal import flow_cli
|
|
134
|
+
from pal.terminal.cli import _bind_status, _fresh
|
|
135
|
+
from pal.terminal.workflow import Workflow
|
|
136
|
+
before = sum(estimate_tokens(m.get("text", "")) for m in wf.plan.b.convo)
|
|
137
|
+
transcript = "\n".join(f"{m.get('role')}: {m.get('text', '')[:3000]}" for m in wf.plan.b.convo[-60:])
|
|
138
|
+
focus = f" Keep in particular: {rest}." if rest else ""
|
|
139
|
+
mode = session.mode
|
|
140
|
+
session.mode = "ask"
|
|
141
|
+
try:
|
|
142
|
+
reply = flow_cli.safely(console, lambda: flow_cli.spin(session, console, "Pal is summarising…", lambda: session.client.ask(
|
|
143
|
+
"Summarise this conversation so it can continue from the summary alone: decisions made, what was built, open questions, "
|
|
144
|
+
f"names of files and functions that matter. Short bullet points, no preface.{focus}\n\n{transcript}")))
|
|
145
|
+
finally:
|
|
146
|
+
session.mode = mode
|
|
147
|
+
if reply is None:
|
|
148
|
+
return session, True
|
|
149
|
+
summary = ui.clean_terminal(reply.text).strip()
|
|
150
|
+
fresh = _fresh(session, args, core_factory)
|
|
151
|
+
wf.plan.b.convo = [{"role": "pal", "agent": "summary", "text": "Summary of the conversation so far:\n" + summary, "t": time.time()}]
|
|
152
|
+
wf.save()
|
|
153
|
+
box["wf"] = Workflow(fresh, wf.store, plan=wf.plan.name)
|
|
154
|
+
_bind_status(box["wf"])
|
|
155
|
+
after = estimate_tokens(summary)
|
|
156
|
+
_say(console, f"Compacted: the conversation is now a summary of about {after:,} tokens (it was about {before:,}).")
|
|
157
|
+
console.print()
|
|
158
|
+
return fresh, True
|
|
159
|
+
|
|
160
|
+
if cmd == "/btw":
|
|
161
|
+
if not rest:
|
|
162
|
+
_say(console, "Use: /btw <a quick question> (not added to the conversation)")
|
|
163
|
+
console.print()
|
|
164
|
+
return session, True
|
|
165
|
+
from pal.terminal import flow_cli
|
|
166
|
+
from pal.terminal.session import make_session
|
|
167
|
+
prov, model = session.model_for("ask")
|
|
168
|
+
try:
|
|
169
|
+
core = (core_factory or backend.make_core)(prov, model, host)
|
|
170
|
+
except Exception as e: # noqa: BLE001
|
|
171
|
+
_say(console, f"Could not start the side question: {e}", theme.RED_HI)
|
|
172
|
+
return session, True
|
|
173
|
+
side = make_session(core, model, prov, session.cwd, "ask", helper=session.helper_core, local=session.local, project=session.project)
|
|
174
|
+
try:
|
|
175
|
+
ui.show_user(console, "btw: " + rest)
|
|
176
|
+
result = flow_cli.safely(console, lambda: flow_cli.spin(side, console, "Pal is answering on the side…", lambda: side.ask(rest)))
|
|
177
|
+
if result is not None:
|
|
178
|
+
ui.show_reply(console, result, side)
|
|
179
|
+
session.pal_tokens += side.pal_tokens
|
|
180
|
+
session.cost_usd += side.cost_usd
|
|
181
|
+
finally:
|
|
182
|
+
side.close()
|
|
183
|
+
_say(console, "(not added to the conversation)")
|
|
184
|
+
console.print()
|
|
185
|
+
return session, True
|
|
186
|
+
|
|
187
|
+
if cmd == "/cost":
|
|
188
|
+
grid = Table.grid(padding=(0, 3))
|
|
189
|
+
rows = [("used", f"{session.pal_tokens:,} tokens"), ("helper (local)", f"{session.helper_tokens:,} tokens"),
|
|
190
|
+
("a plain chat would have used", f"about {session.plain_tokens:,} tokens"),
|
|
191
|
+
("saved", f"about {max(0, session.saved):,} tokens ({session.saved_pct:g}%)"),
|
|
192
|
+
("cost", f"${session.cost_usd:.4f}" if session.cost_usd else "no price known (local models are free)"),
|
|
193
|
+
("requests", str(session.turns)), ("time with the model", f"{session.seconds:.0f}s")]
|
|
194
|
+
for k, v in rows:
|
|
195
|
+
grid.add_row(Text(k, style=theme.MUTED), Text(v, style=f"bold {theme.WHITE}"))
|
|
196
|
+
ui.say(console, grid)
|
|
197
|
+
_say(console, "Where the savings came from: /tokens")
|
|
198
|
+
console.print()
|
|
199
|
+
return session, True
|
|
200
|
+
|
|
201
|
+
if cmd == "/context":
|
|
202
|
+
parts: list[tuple[str, int, str]] = []
|
|
203
|
+
if session.project is not None:
|
|
204
|
+
r = session.project.report
|
|
205
|
+
parts.append(("project map (indexed)", r.map_tokens, "only matching lines are sent"))
|
|
206
|
+
docs = sum(d.tokens for d in r.docs if not d.error)
|
|
207
|
+
if docs:
|
|
208
|
+
parts.append((f"documents, {len(r.docs)} (indexed)", docs, "only matching passages are sent"))
|
|
209
|
+
parts.append(("code graph", 0, r.graph))
|
|
210
|
+
notes = session.project.notes()
|
|
211
|
+
if notes:
|
|
212
|
+
parts.append((NOTES_FILE, estimate_tokens(notes), "sent once per session"))
|
|
213
|
+
if wf is not None:
|
|
214
|
+
rows = wf.context_rows()
|
|
215
|
+
if rows:
|
|
216
|
+
parts.append((f"loaded context, {len(rows)} item(s)", sum(row[2] for row in rows), "sent when it changes"))
|
|
217
|
+
if wf.plan.b.pending:
|
|
218
|
+
parts.append((f"pending changes, {len(wf.plan.b.pending)} file(s)", sum(estimate_tokens(v) for v in wf.plan.b.pending.values()), "diffs are taken against these"))
|
|
219
|
+
parts.append(("conversation so far", session.history_tokens, "recent turns sent, older ones only when relevant"))
|
|
220
|
+
top = max((n for _, n, _ in parts), default=1) or 1
|
|
221
|
+
grid = Table.grid(padding=(0, 2))
|
|
222
|
+
for name, n, note in parts:
|
|
223
|
+
bar = "█" * max(1, round(24 * n / top)) if n else ""
|
|
224
|
+
grid.add_row(Text(name, style=theme.WHITE), Text(f"{n:,}" if n else "-", style=theme.INK, justify="right"),
|
|
225
|
+
Text(bar, style=theme.RED), Text(note, style=theme.MUTED))
|
|
226
|
+
ui.say(console, grid)
|
|
227
|
+
_say(console, "Tokens are estimates (characters / 4). /cost for the totals, /tokens for the savings.")
|
|
228
|
+
console.print()
|
|
229
|
+
return session, True
|
|
230
|
+
|
|
231
|
+
if cmd == "/memory":
|
|
232
|
+
f = session.cwd / NOTES_FILE
|
|
233
|
+
if rest.strip() == "edit":
|
|
234
|
+
editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or ("notepad" if os.name == "nt" else "nano" if shutil.which("nano") else "vi")
|
|
235
|
+
if not f.exists():
|
|
236
|
+
f.write_text(f"# {session.cwd.name}\n\nInstructions for Pal in this project.\n", encoding="utf-8")
|
|
237
|
+
subprocess.call([*editor.split(), str(f)]) # noqa: S603 (your own editor, on your own file)
|
|
238
|
+
session.brief = session.project.brief() if session.project is not None else session.brief
|
|
239
|
+
_say(console, f"{NOTES_FILE} saved. It goes with the first request of every session.")
|
|
240
|
+
elif f.is_file():
|
|
241
|
+
ui.say(console, Text(f.read_text(encoding="utf-8", errors="replace")[:4000], style=theme.INK))
|
|
242
|
+
_say(console, f"/memory edit opens it in your editor ({ui.short_path(f)})")
|
|
243
|
+
else:
|
|
244
|
+
_say(console, f"No {NOTES_FILE} yet. /init starts one from the project, /memory edit writes one yourself.")
|
|
245
|
+
console.print()
|
|
246
|
+
return session, True
|
|
247
|
+
|
|
248
|
+
if cmd == "/add-dir":
|
|
249
|
+
if wf is None or not rest:
|
|
250
|
+
_say(console, "Use: /add-dir <folder> its files join the context (read-only; secrets stay hidden)")
|
|
251
|
+
console.print()
|
|
252
|
+
return session, True
|
|
253
|
+
target = Path(rest.strip().strip("\"'")).expanduser().resolve()
|
|
254
|
+
if not target.is_dir():
|
|
255
|
+
_say(console, f"{rest}: not a folder", theme.RED_HI)
|
|
256
|
+
else:
|
|
257
|
+
added, problems = wf.load([str(target)])
|
|
258
|
+
_say(console, f"Added {len(added)} file(s) from {ui.short_path(target)} to the context." + (f" {'; '.join(problems)}" if problems else ""))
|
|
259
|
+
console.print()
|
|
260
|
+
return session, True
|
|
261
|
+
|
|
262
|
+
if cmd == "/permissions":
|
|
263
|
+
from pal.terminal import flow_cli
|
|
264
|
+
flow_cli.handle_flow("/autonomy", rest, wf, console) if wf is not None else None
|
|
265
|
+
return session, True
|
|
266
|
+
|
|
267
|
+
if cmd in ("/config", "/settings"):
|
|
268
|
+
grid = Table.grid(padding=(0, 3))
|
|
269
|
+
rows = [("main model", f"{session.model} ({session.provider})")]
|
|
270
|
+
rows += [(f"{m} agent", f"{session.model_for(m)[1]} ({session.model_for(m)[0]})" + ("" if m in session.role_models else " (main)")) for m in MODES]
|
|
271
|
+
rows += [("helper", session.helper_model or "none"), ("custom endpoint", backend.custom_base_url() or "none"),
|
|
272
|
+
("autonomy", wf.plan.autonomy if wf else "-"), ("token cap", f"{session.budget:,}" if session.budget else "none"),
|
|
273
|
+
("preferences", str(backend.prefs_path())), ("project data", str(wf.store.root) if wf else "-")]
|
|
274
|
+
for k, v in rows:
|
|
275
|
+
grid.add_row(Text(k, style=theme.MUTED), Text(v, style=theme.WHITE))
|
|
276
|
+
ui.say(console, grid)
|
|
277
|
+
_say(console, "Change: /model [agent] <provider>:<model>, /endpoint <url>, /autonomy <level>, /set <setting> on|off, /budget <tokens>. Keys are never saved.")
|
|
278
|
+
console.print()
|
|
279
|
+
return session, True
|
|
280
|
+
|
|
281
|
+
if cmd == "/copy":
|
|
282
|
+
text = session.last_reply
|
|
283
|
+
if not text:
|
|
284
|
+
_say(console, "Nothing to copy yet.")
|
|
285
|
+
else:
|
|
286
|
+
how = copy_to_clipboard(text)
|
|
287
|
+
_say(console, f"Copied the last answer ({len(text):,} characters) with {how}." if how else "No clipboard here (install xclip or wl-copy).")
|
|
288
|
+
console.print()
|
|
289
|
+
return session, True
|
|
290
|
+
|
|
291
|
+
if cmd == "/export":
|
|
292
|
+
if wf is None:
|
|
293
|
+
return session, True
|
|
294
|
+
out = Path(rest.strip()).expanduser() if rest.strip() else session.cwd / f"pal-{wf.plan.name}-{time.strftime('%Y%m%d-%H%M')}.md"
|
|
295
|
+
if not out.is_absolute():
|
|
296
|
+
out = session.cwd / out
|
|
297
|
+
try:
|
|
298
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
299
|
+
out.write_text(conversation_markdown(wf), encoding="utf-8")
|
|
300
|
+
_say(console, f"Saved the conversation to {ui.short_path(out)}")
|
|
301
|
+
except OSError as e:
|
|
302
|
+
_say(console, f"Could not save: {e.strerror or e}", theme.RED_HI)
|
|
303
|
+
console.print()
|
|
304
|
+
return session, True
|
|
305
|
+
|
|
306
|
+
if cmd == "/agents":
|
|
307
|
+
from pal.terminal.session import AGENT_ABOUT
|
|
308
|
+
grid = Table.grid(padding=(0, 3))
|
|
309
|
+
for m in MODES:
|
|
310
|
+
prov, model = session.model_for(m)
|
|
311
|
+
grid.add_row(Text(m.capitalize(), style=f"bold {theme.RED_HI if m == session.mode else theme.WHITE}"), Text(f"{model} ({prov})", style=theme.WHITE),
|
|
312
|
+
Text(AGENT_ABOUT.get(m, ""), style=theme.MUTED))
|
|
313
|
+
if session.helper_model:
|
|
314
|
+
grid.add_row(Text("helper", style=theme.MUTED), Text(session.helper_model, style=theme.WHITE), Text("Pal's own small jobs: routing, trimming, checking", style=theme.MUTED))
|
|
315
|
+
ui.say(console, grid)
|
|
316
|
+
_say(console, "Give an agent its own model: /model plan <provider>:<model> tab switches agent")
|
|
317
|
+
console.print()
|
|
318
|
+
return session, True
|
|
319
|
+
|
|
320
|
+
if cmd == "/simplify":
|
|
321
|
+
if wf is None:
|
|
322
|
+
return session, True
|
|
323
|
+
from pal.terminal import flow_cli
|
|
324
|
+
from pal.terminal.workflow import is_git_repo
|
|
325
|
+
files: list[str] = []
|
|
326
|
+
if is_git_repo(session.cwd):
|
|
327
|
+
r = subprocess.run(["git", "diff", "--name-only", "HEAD~1", "HEAD"], cwd=session.cwd, capture_output=True, text=True) # noqa: S607
|
|
328
|
+
r2 = subprocess.run(["git", "diff", "--name-only"], cwd=session.cwd, capture_output=True, text=True) # noqa: S607
|
|
329
|
+
files = sorted({x for x in (r.stdout + r2.stdout).split() if (session.cwd / x).is_file()})
|
|
330
|
+
files = sorted(set(files) | set(wf.plan.b.pending))[:12]
|
|
331
|
+
if not files:
|
|
332
|
+
_say(console, "No recently changed files found (it looks at the last commit, uncommitted changes and the sandbox).")
|
|
333
|
+
console.print()
|
|
334
|
+
return session, True
|
|
335
|
+
mode = session.mode
|
|
336
|
+
session.mode = "build"
|
|
337
|
+
try:
|
|
338
|
+
flow_cli.run_turn(wf, "Review these recently changed files for reuse (duplicated code), quality (naming, structure, error handling) and efficiency, "
|
|
339
|
+
"then fix what is worth fixing without changing behaviour: " + " ".join(f"@{f}" for f in files) + (f". Focus: {rest}" if rest else ""), console)
|
|
340
|
+
finally:
|
|
341
|
+
session.mode = mode
|
|
342
|
+
return session, True
|
|
343
|
+
|
|
344
|
+
if cmd == "/status":
|
|
345
|
+
hw = backend.hardware()
|
|
346
|
+
grid = Table.grid(padding=(0, 3))
|
|
347
|
+
rows = [("pal", __version__), ("folder", str(session.cwd)), ("agent", session.mode), ("model", f"{session.model_for()[1]} ({session.model_for()[0]})"),
|
|
348
|
+
("local", session.local_status or ("off (no GPU)" if not session.local else "-")), ("this computer", hw.summary())]
|
|
349
|
+
if wf is not None:
|
|
350
|
+
rows += [("plan", f"{wf.plan.name} / {wf.plan.branch}"), ("autonomy", wf.plan.autonomy), ("pending", f"{len(wf.plan.b.pending)} file(s)")]
|
|
351
|
+
rows += [("tokens", f"{session.pal_tokens:,} used, about {max(0, session.saved):,} saved")]
|
|
352
|
+
for k, v in rows:
|
|
353
|
+
grid.add_row(Text(k, style=theme.MUTED), Text(v, style=theme.WHITE))
|
|
354
|
+
ui.say(console, grid)
|
|
355
|
+
console.print()
|
|
356
|
+
return session, True
|
|
357
|
+
|
|
358
|
+
if cmd == "/doctor":
|
|
359
|
+
from pal.cli import main as tools_main
|
|
360
|
+
try:
|
|
361
|
+
tools_main(["doctor"])
|
|
362
|
+
except SystemExit:
|
|
363
|
+
pass
|
|
364
|
+
console.print()
|
|
365
|
+
return session, True
|
|
366
|
+
|
|
367
|
+
if cmd == "/resume":
|
|
368
|
+
if wf is None:
|
|
369
|
+
return session, True
|
|
370
|
+
if not rest:
|
|
371
|
+
names = wf.store.names()
|
|
372
|
+
for i, name in enumerate(names, 1):
|
|
373
|
+
b = wf.store.load(name).b
|
|
374
|
+
last = (b.convo[-1]["text"][:60] + "…") if b.convo else "no conversation yet"
|
|
375
|
+
ui.say(console, Text.assemble((f"{i:>3} {name:<16}", f"bold {theme.WHITE}" if name == wf.plan.name else theme.INK), (last, theme.MUTED)))
|
|
376
|
+
_say(console, "/resume <name or number> picks one up where it stopped")
|
|
377
|
+
console.print()
|
|
378
|
+
return session, True
|
|
379
|
+
from pal.terminal.cli import handle
|
|
380
|
+
return handle(f"/cd {rest}", args, session, console, core_factory, box)
|
|
381
|
+
|
|
382
|
+
if cmd == "/init":
|
|
383
|
+
if wf is None:
|
|
384
|
+
return session, True
|
|
385
|
+
from pal.terminal import flow_cli
|
|
386
|
+
mode = session.mode
|
|
387
|
+
session.mode = "build"
|
|
388
|
+
exists = (session.cwd / NOTES_FILE).is_file()
|
|
389
|
+
try:
|
|
390
|
+
flow_cli.run_turn(wf, (f"{'Improve' if exists else 'Write'} {NOTES_FILE} for this project: what it is, how it is laid out, how to run and test it, the "
|
|
391
|
+
"conventions the code follows, and anything a new developer (or Pal) must not get wrong. Use the project map, the documents "
|
|
392
|
+
f"and the files you can see. Short sections, bullet points, under 120 lines. Reply with the complete {NOTES_FILE} only."), console)
|
|
393
|
+
finally:
|
|
394
|
+
session.mode = mode
|
|
395
|
+
_say(console, f"Review it with /diff, write it with /apply. {NOTES_FILE} then goes with the first request of every session.")
|
|
396
|
+
console.print()
|
|
397
|
+
return session, True
|
|
398
|
+
|
|
399
|
+
if cmd == "/voice":
|
|
400
|
+
return None # the loop handles it (it needs the input box)
|
|
401
|
+
return None
|