oshell 0.1.1__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.
oshell/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """oshell — a fast, beautiful terminal chat for any LLM."""
2
+
3
+ __version__ = "0.1.1"
oshell/agent.py ADDED
@@ -0,0 +1,146 @@
1
+ """The oshell coding agent.
2
+
3
+ A small but capable ReAct-style loop: the model is asked to reply with a
4
+ single JSON object describing one action at a time. We execute the action,
5
+ feed the result back as an observation, and repeat until the model calls
6
+ ``finish`` (or we hit the step limit).
7
+
8
+ This JSON protocol works with *any* chat model — it does not rely on
9
+ provider-specific function/tool calling, so the same agent runs on local
10
+ Ollama models and on OpenAI alike.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from typing import Iterator, List
18
+
19
+ from .providers import Message, Provider
20
+ from .tools import TOOL_REFERENCE, Toolbox
21
+
22
+ MAX_STEPS = 25
23
+
24
+ SYSTEM_PROMPT = f"""\
25
+ You are oshell Agent, an autonomous coding assistant working inside a user's \
26
+ project directory. You accomplish the user's task by taking ONE action at a \
27
+ time using the available tools.
28
+
29
+ {TOOL_REFERENCE}
30
+
31
+ RESPONSE FORMAT — respond with a SINGLE JSON object and nothing else:
32
+ {{"thought": "brief reasoning about the next step",
33
+ "action": "<one tool name above>",
34
+ "args": {{ ...arguments for that tool... }}}}
35
+
36
+ Rules:
37
+ - Output ONLY the JSON object. No markdown, no code fences, no extra prose.
38
+ - Take one action per response. Wait for the observation before continuing.
39
+ - Explore with list_dir / read_file before editing unfamiliar files.
40
+ - When writing a file, include its COMPLETE intended contents in "content".
41
+ - When the task is done, use the "finish" action with a short summary.
42
+ - Keep commands cross-platform where possible.
43
+ """
44
+
45
+
46
+ def _extract_json(text: str) -> dict:
47
+ """Best-effort parse of a JSON object from a model response."""
48
+ text = text.strip()
49
+ # Strip code fences if the model added them anyway.
50
+ if text.startswith("```"):
51
+ text = re.sub(r"^```[a-zA-Z]*\n?", "", text)
52
+ text = re.sub(r"\n?```$", "", text).strip()
53
+ try:
54
+ return json.loads(text)
55
+ except json.JSONDecodeError:
56
+ pass
57
+ # Fall back to the first balanced {...} block.
58
+ start = text.find("{")
59
+ if start == -1:
60
+ raise ValueError("No JSON object found in model response.")
61
+ depth = 0
62
+ for i in range(start, len(text)):
63
+ if text[i] == "{":
64
+ depth += 1
65
+ elif text[i] == "}":
66
+ depth -= 1
67
+ if depth == 0:
68
+ return json.loads(text[start : i + 1])
69
+ raise ValueError("Unbalanced JSON in model response.")
70
+
71
+
72
+ class AgentEvent:
73
+ """A step in the agent's run, yielded for live display."""
74
+
75
+ def __init__(self, kind: str, **data):
76
+ self.kind = kind # "thought" | "action" | "observation" | "finish" | "error"
77
+ self.data = data
78
+
79
+
80
+ class CodingAgent:
81
+ """Drives the think-act-observe loop."""
82
+
83
+ def __init__(self, provider: Provider, toolbox: Toolbox, max_steps: int = MAX_STEPS):
84
+ self.provider = provider
85
+ self.toolbox = toolbox
86
+ self.max_steps = max_steps
87
+ # Conversation state persists across tasks so the agent can take
88
+ # follow-up instructions in interactive mode while remembering what
89
+ # it already did.
90
+ self.messages: List[Message] = [
91
+ {"role": "system", "content": SYSTEM_PROMPT}
92
+ ]
93
+
94
+ def _complete(self, messages: List[Message]) -> str:
95
+ """Collect a full (non-streamed) completion from the provider."""
96
+ return "".join(self.provider.stream_chat(messages))
97
+
98
+ def run(self, task: str) -> Iterator[AgentEvent]:
99
+ """Run the agent on ``task``, yielding events as it progresses.
100
+
101
+ Conversation history is retained between calls, so calling ``run``
102
+ again with a follow-up continues the same session.
103
+ """
104
+ self.messages.append({"role": "user", "content": f"Task: {task}"})
105
+
106
+ for _ in range(self.max_steps):
107
+ raw = self._complete(self.messages)
108
+ try:
109
+ step = _extract_json(raw)
110
+ except ValueError as exc:
111
+ yield AgentEvent("error", message=f"{exc}\nModel said: {raw[:300]}")
112
+ # Nudge the model back to the protocol.
113
+ self.messages.append({"role": "assistant", "content": raw})
114
+ self.messages.append(
115
+ {
116
+ "role": "user",
117
+ "content": "Your last message was not valid JSON. "
118
+ "Respond with exactly one JSON object as instructed.",
119
+ }
120
+ )
121
+ continue
122
+
123
+ thought = step.get("thought", "")
124
+ action = step.get("action", "")
125
+ args = step.get("args", {}) or {}
126
+
127
+ if thought:
128
+ yield AgentEvent("thought", text=thought)
129
+
130
+ self.messages.append({"role": "assistant", "content": json.dumps(step)})
131
+
132
+ if action == "finish":
133
+ yield AgentEvent("finish", summary=args.get("summary", "Done."))
134
+ return
135
+
136
+ yield AgentEvent("action", action=action, args=args)
137
+ result = self.toolbox.dispatch(action, args)
138
+ yield AgentEvent("observation", ok=result.ok, output=result.output)
139
+
140
+ self.messages.append(
141
+ {"role": "user", "content": f"Observation: {result.render()}"}
142
+ )
143
+
144
+ yield AgentEvent(
145
+ "error", message=f"Reached the step limit ({self.max_steps}). Stopping."
146
+ )