simple-agents-framework 0.1.0__tar.gz
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.
- simple_agents_framework-0.1.0/PKG-INFO +60 -0
- simple_agents_framework-0.1.0/README.md +50 -0
- simple_agents_framework-0.1.0/pyproject.toml +18 -0
- simple_agents_framework-0.1.0/setup.cfg +4 -0
- simple_agents_framework-0.1.0/simple_agents_framework.egg-info/PKG-INFO +60 -0
- simple_agents_framework-0.1.0/simple_agents_framework.egg-info/SOURCES.txt +8 -0
- simple_agents_framework-0.1.0/simple_agents_framework.egg-info/dependency_links.txt +1 -0
- simple_agents_framework-0.1.0/simple_agents_framework.egg-info/requires.txt +1 -0
- simple_agents_framework-0.1.0/simple_agents_framework.egg-info/top_level.txt +1 -0
- simple_agents_framework-0.1.0/simple_agents_framework.py +261 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_agents_framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A markdown file in, a callable agent out. One Python file over the Claude Agent SDK.
|
|
5
|
+
Author: Agentsable
|
|
6
|
+
Project-URL: Homepage, https://github.com/Agentsable/simple_agents_framework
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: claude-agent-sdk>=0.1.52
|
|
10
|
+
|
|
11
|
+
# simple_agents_framework
|
|
12
|
+
|
|
13
|
+
A markdown file in, a callable agent out. One Python file over the
|
|
14
|
+
[Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python).
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import simple_agents_framework as saf
|
|
18
|
+
|
|
19
|
+
agent = saf.create_agent_from_markdown("doc_reader.md", API_KEY)
|
|
20
|
+
agent.ask("which markdown files are here?") # -> str
|
|
21
|
+
agent.ask_html("...") # same, streamed as HTML in Jupyter
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Agent files
|
|
25
|
+
|
|
26
|
+
Frontmatter configures, body is the system prompt. Every key is optional:
|
|
27
|
+
|
|
28
|
+
| key | effect |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `name` | agent name (defaults to the filename) |
|
|
31
|
+
| `model` | e.g. `claude-sonnet-5` |
|
|
32
|
+
| `tools` | comma-separated allowlist, e.g. `Read, Grep, Glob` |
|
|
33
|
+
| `permission_mode` | defaults to `bypassPermissions` — headless agents can't answer prompts |
|
|
34
|
+
|
|
35
|
+
Anything else passes through as a `ClaudeAgentOptions` kwarg:
|
|
36
|
+
`create_agent_from_markdown(path, key, cwd="/repo", max_turns=5)`.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
- `ask(prompt) -> str` — blocks, works inside Jupyter too.
|
|
41
|
+
- `ask_html(prompt) -> str` — same run, streamed into the cell: blue sent,
|
|
42
|
+
green agent, slate thinking, amber tool call, cyan tool result, red error.
|
|
43
|
+
The agent's text fills in token by token; tool calls and results appear whole.
|
|
44
|
+
- `ask_async(prompt, on_event=None)` — `on_event(kind, title, body, replace)`
|
|
45
|
+
per update. `replace=True` means "same block, more text": redraw the last
|
|
46
|
+
thing you drew instead of appending. Build your own renderer on this.
|
|
47
|
+
|
|
48
|
+
Follow-up asks resume the previous session, so context carries over.
|
|
49
|
+
|
|
50
|
+
## Files
|
|
51
|
+
|
|
52
|
+
- `simple_agents_framework.py` — the whole thing. `python simple_agents_framework.py` runs its self-check.
|
|
53
|
+
- `doc_reader.md` — example agent: reads the markdown in a project and answers questions about it.
|
|
54
|
+
- `demo.ipynb` — end-to-end walkthrough with streamed output.
|
|
55
|
+
|
|
56
|
+
## Gotcha
|
|
57
|
+
|
|
58
|
+
The SDK spawns whatever `claude` is first on `PATH`. Some tools install a
|
|
59
|
+
wrapper there that never returns headlessly — if `ask()` hangs, pass
|
|
60
|
+
`cli_path=Path.home() / ".local/bin/claude"`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# simple_agents_framework
|
|
2
|
+
|
|
3
|
+
A markdown file in, a callable agent out. One Python file over the
|
|
4
|
+
[Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python).
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import simple_agents_framework as saf
|
|
8
|
+
|
|
9
|
+
agent = saf.create_agent_from_markdown("doc_reader.md", API_KEY)
|
|
10
|
+
agent.ask("which markdown files are here?") # -> str
|
|
11
|
+
agent.ask_html("...") # same, streamed as HTML in Jupyter
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Agent files
|
|
15
|
+
|
|
16
|
+
Frontmatter configures, body is the system prompt. Every key is optional:
|
|
17
|
+
|
|
18
|
+
| key | effect |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `name` | agent name (defaults to the filename) |
|
|
21
|
+
| `model` | e.g. `claude-sonnet-5` |
|
|
22
|
+
| `tools` | comma-separated allowlist, e.g. `Read, Grep, Glob` |
|
|
23
|
+
| `permission_mode` | defaults to `bypassPermissions` — headless agents can't answer prompts |
|
|
24
|
+
|
|
25
|
+
Anything else passes through as a `ClaudeAgentOptions` kwarg:
|
|
26
|
+
`create_agent_from_markdown(path, key, cwd="/repo", max_turns=5)`.
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
- `ask(prompt) -> str` — blocks, works inside Jupyter too.
|
|
31
|
+
- `ask_html(prompt) -> str` — same run, streamed into the cell: blue sent,
|
|
32
|
+
green agent, slate thinking, amber tool call, cyan tool result, red error.
|
|
33
|
+
The agent's text fills in token by token; tool calls and results appear whole.
|
|
34
|
+
- `ask_async(prompt, on_event=None)` — `on_event(kind, title, body, replace)`
|
|
35
|
+
per update. `replace=True` means "same block, more text": redraw the last
|
|
36
|
+
thing you drew instead of appending. Build your own renderer on this.
|
|
37
|
+
|
|
38
|
+
Follow-up asks resume the previous session, so context carries over.
|
|
39
|
+
|
|
40
|
+
## Files
|
|
41
|
+
|
|
42
|
+
- `simple_agents_framework.py` — the whole thing. `python simple_agents_framework.py` runs its self-check.
|
|
43
|
+
- `doc_reader.md` — example agent: reads the markdown in a project and answers questions about it.
|
|
44
|
+
- `demo.ipynb` — end-to-end walkthrough with streamed output.
|
|
45
|
+
|
|
46
|
+
## Gotcha
|
|
47
|
+
|
|
48
|
+
The SDK spawns whatever `claude` is first on `PATH`. Some tools install a
|
|
49
|
+
wrapper there that never returns headlessly — if `ask()` hangs, pass
|
|
50
|
+
`cli_path=Path.home() / ".local/bin/claude"`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simple_agents_framework"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A markdown file in, a callable agent out. One Python file over the Claude Agent SDK."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Agentsable" }]
|
|
12
|
+
dependencies = ["claude-agent-sdk>=0.1.52"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/Agentsable/simple_agents_framework"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools]
|
|
18
|
+
py-modules = ["simple_agents_framework"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_agents_framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A markdown file in, a callable agent out. One Python file over the Claude Agent SDK.
|
|
5
|
+
Author: Agentsable
|
|
6
|
+
Project-URL: Homepage, https://github.com/Agentsable/simple_agents_framework
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: claude-agent-sdk>=0.1.52
|
|
10
|
+
|
|
11
|
+
# simple_agents_framework
|
|
12
|
+
|
|
13
|
+
A markdown file in, a callable agent out. One Python file over the
|
|
14
|
+
[Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-python).
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import simple_agents_framework as saf
|
|
18
|
+
|
|
19
|
+
agent = saf.create_agent_from_markdown("doc_reader.md", API_KEY)
|
|
20
|
+
agent.ask("which markdown files are here?") # -> str
|
|
21
|
+
agent.ask_html("...") # same, streamed as HTML in Jupyter
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Agent files
|
|
25
|
+
|
|
26
|
+
Frontmatter configures, body is the system prompt. Every key is optional:
|
|
27
|
+
|
|
28
|
+
| key | effect |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `name` | agent name (defaults to the filename) |
|
|
31
|
+
| `model` | e.g. `claude-sonnet-5` |
|
|
32
|
+
| `tools` | comma-separated allowlist, e.g. `Read, Grep, Glob` |
|
|
33
|
+
| `permission_mode` | defaults to `bypassPermissions` — headless agents can't answer prompts |
|
|
34
|
+
|
|
35
|
+
Anything else passes through as a `ClaudeAgentOptions` kwarg:
|
|
36
|
+
`create_agent_from_markdown(path, key, cwd="/repo", max_turns=5)`.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
- `ask(prompt) -> str` — blocks, works inside Jupyter too.
|
|
41
|
+
- `ask_html(prompt) -> str` — same run, streamed into the cell: blue sent,
|
|
42
|
+
green agent, slate thinking, amber tool call, cyan tool result, red error.
|
|
43
|
+
The agent's text fills in token by token; tool calls and results appear whole.
|
|
44
|
+
- `ask_async(prompt, on_event=None)` — `on_event(kind, title, body, replace)`
|
|
45
|
+
per update. `replace=True` means "same block, more text": redraw the last
|
|
46
|
+
thing you drew instead of appending. Build your own renderer on this.
|
|
47
|
+
|
|
48
|
+
Follow-up asks resume the previous session, so context carries over.
|
|
49
|
+
|
|
50
|
+
## Files
|
|
51
|
+
|
|
52
|
+
- `simple_agents_framework.py` — the whole thing. `python simple_agents_framework.py` runs its self-check.
|
|
53
|
+
- `doc_reader.md` — example agent: reads the markdown in a project and answers questions about it.
|
|
54
|
+
- `demo.ipynb` — end-to-end walkthrough with streamed output.
|
|
55
|
+
|
|
56
|
+
## Gotcha
|
|
57
|
+
|
|
58
|
+
The SDK spawns whatever `claude` is first on `PATH`. Some tools install a
|
|
59
|
+
wrapper there that never returns headlessly — if `ask()` hangs, pass
|
|
60
|
+
`cli_path=Path.home() / ".local/bin/claude"`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
simple_agents_framework.py
|
|
4
|
+
simple_agents_framework.egg-info/PKG-INFO
|
|
5
|
+
simple_agents_framework.egg-info/SOURCES.txt
|
|
6
|
+
simple_agents_framework.egg-info/dependency_links.txt
|
|
7
|
+
simple_agents_framework.egg-info/requires.txt
|
|
8
|
+
simple_agents_framework.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
claude-agent-sdk>=0.1.52
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simple_agents_framework
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Markdown-defined agents on top of the Claude Agent SDK.
|
|
2
|
+
|
|
3
|
+
import simple_agents_framework as saf
|
|
4
|
+
agent = saf.create_agent_from_markdown("researcher.md", anthropic_api_key)
|
|
5
|
+
print(agent.ask("what changed in the repo today?"))
|
|
6
|
+
|
|
7
|
+
In a Jupyter notebook, agent.ask_html(...) streams the same run as color-coded
|
|
8
|
+
HTML: the prompt, the agent's text as it is typed, each tool call, and each
|
|
9
|
+
tool result.
|
|
10
|
+
|
|
11
|
+
Markdown file = optional YAML-ish frontmatter + body (the system prompt):
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
name: researcher
|
|
15
|
+
description: digs through code
|
|
16
|
+
model: claude-opus-5
|
|
17
|
+
tools: Read, Grep, Glob
|
|
18
|
+
---
|
|
19
|
+
You are a careful code researcher. Answer with file:line references.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import html
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from claude_agent_sdk import (
|
|
32
|
+
AssistantMessage,
|
|
33
|
+
ClaudeAgentOptions,
|
|
34
|
+
ResultMessage,
|
|
35
|
+
TextBlock,
|
|
36
|
+
StreamEvent,
|
|
37
|
+
ThinkingBlock,
|
|
38
|
+
ToolResultBlock,
|
|
39
|
+
ToolUseBlock,
|
|
40
|
+
query,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = ["Agent", "create_agent_from_markdown"]
|
|
44
|
+
|
|
45
|
+
# label -> accent color, one per kind of thing that shows up in a run.
|
|
46
|
+
COLORS = {
|
|
47
|
+
"sent": "#2563eb", # blue - what you sent
|
|
48
|
+
"received": "#16a34a", # green - what the agent said
|
|
49
|
+
"thinking": "#64748b", # slate - reasoning
|
|
50
|
+
"tool use": "#d97706", # amber - tool call
|
|
51
|
+
"tool result": "#0891b2", # cyan - tool output
|
|
52
|
+
"error": "#dc2626", # red
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_loop = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run(coro):
|
|
60
|
+
"""Run a coroutine on one shared background loop, from sync code.
|
|
61
|
+
|
|
62
|
+
ponytail: a background loop rather than asyncio.run per call — that closes
|
|
63
|
+
the child-process watcher every time (noisy warnings) and blows up inside
|
|
64
|
+
Jupyter, which already owns the main loop.
|
|
65
|
+
"""
|
|
66
|
+
global _loop
|
|
67
|
+
if _loop is None:
|
|
68
|
+
_loop = asyncio.new_event_loop()
|
|
69
|
+
threading.Thread(target=_loop.run_forever, daemon=True).start()
|
|
70
|
+
return asyncio.run_coroutine_threadsafe(coro, _loop).result()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _card(kind, title, body):
|
|
74
|
+
color = COLORS[kind] # kind is always one of the six above
|
|
75
|
+
return (
|
|
76
|
+
f'<div style="border-left:3px solid {color};background:{color}14;'
|
|
77
|
+
'padding:6px 10px;margin:4px 0;border-radius:0 4px 4px 0;'
|
|
78
|
+
'font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px">'
|
|
79
|
+
f'<div style="color:{color};font-weight:600;letter-spacing:.04em;'
|
|
80
|
+
f'text-transform:uppercase;font-size:10.5px">{html.escape(title)}</div>'
|
|
81
|
+
f'<div style="white-space:pre-wrap;color:inherit;opacity:.9">{html.escape(body)}</div>'
|
|
82
|
+
"</div>"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clip(text, limit=1200):
|
|
87
|
+
if isinstance(text, list): # tool results arrive as content blocks
|
|
88
|
+
text = "\n".join(b.get("text", str(b)) if isinstance(b, dict) else str(b) for b in text)
|
|
89
|
+
text = str(text).strip()
|
|
90
|
+
return text if len(text) <= limit else text[:limit] + f"\n… (+{len(text) - limit} chars)"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_markdown(text):
|
|
94
|
+
"""-> (metadata dict, system prompt). Frontmatter is flat `key: value` lines."""
|
|
95
|
+
meta, body = {}, text
|
|
96
|
+
if text.lstrip().startswith("---"):
|
|
97
|
+
_, front, body = text.lstrip().split("---", 2)
|
|
98
|
+
meta = {k: v.strip() for k, v in re.findall(r"^(\w+)\s*:\s*(.*)$", front, re.M)}
|
|
99
|
+
return meta, body.strip()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Agent:
|
|
103
|
+
def __init__(self, name, options):
|
|
104
|
+
self.name = name
|
|
105
|
+
self.options = options
|
|
106
|
+
|
|
107
|
+
def ask(self, prompt):
|
|
108
|
+
"""Send a prompt, return the agent's final text. Blocks until done."""
|
|
109
|
+
return _run(self.ask_async(prompt))
|
|
110
|
+
|
|
111
|
+
def ask_html(self, prompt):
|
|
112
|
+
"""Same, but stream the run into a Jupyter cell as color-coded HTML."""
|
|
113
|
+
from IPython.display import HTML, display
|
|
114
|
+
|
|
115
|
+
cards = [_card("sent", "sent", prompt)]
|
|
116
|
+
handle = display(HTML("".join(cards)), display_id=True)
|
|
117
|
+
state = {"open": False, "drawn": 0.0}
|
|
118
|
+
|
|
119
|
+
def flush(force=False):
|
|
120
|
+
# ponytail: ~20fps. Every token would be its own display update,
|
|
121
|
+
# each re-joining the whole card list. force=True on the last draw.
|
|
122
|
+
if force or time.monotonic() - state["drawn"] > 0.05:
|
|
123
|
+
state["drawn"] = time.monotonic()
|
|
124
|
+
handle.update(HTML("".join(cards)))
|
|
125
|
+
|
|
126
|
+
def on_event(kind, title, body, replace):
|
|
127
|
+
card = _card(kind, title, body)
|
|
128
|
+
if replace and state["open"]:
|
|
129
|
+
cards[-1] = card
|
|
130
|
+
else:
|
|
131
|
+
cards.append(card)
|
|
132
|
+
state["open"] = replace
|
|
133
|
+
flush(force=not replace)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
return _run(self.ask_async(prompt, on_event))
|
|
137
|
+
finally:
|
|
138
|
+
flush(force=True) # the throttle may have skipped the last tokens
|
|
139
|
+
|
|
140
|
+
async def ask_async(self, prompt, on_event=None):
|
|
141
|
+
"""Run the prompt. on_event(kind, title, body, replace) per update;
|
|
142
|
+
replace=True means "same block, more text" — redraw, don't append."""
|
|
143
|
+
emit = on_event or (lambda *a: None)
|
|
144
|
+
partial = self.options.include_partial_messages
|
|
145
|
+
text, buf = [], ""
|
|
146
|
+
async for message in query(prompt=prompt, options=self.options):
|
|
147
|
+
if isinstance(message, StreamEvent):
|
|
148
|
+
event = message.event
|
|
149
|
+
if event.get("type") == "content_block_delta":
|
|
150
|
+
delta = event.get("delta", {})
|
|
151
|
+
chunk = delta.get("text") or delta.get("thinking") or ""
|
|
152
|
+
if chunk: # tool-input deltas have neither; they land whole below
|
|
153
|
+
buf += chunk
|
|
154
|
+
thinking = delta.get("type") == "thinking_delta"
|
|
155
|
+
emit(*(("thinking", "thinking") if thinking
|
|
156
|
+
else ("received", self.name)), buf, True)
|
|
157
|
+
elif event.get("type") == "content_block_stop":
|
|
158
|
+
buf = ""
|
|
159
|
+
continue
|
|
160
|
+
for block in getattr(message, "content", []) or []:
|
|
161
|
+
if isinstance(block, TextBlock):
|
|
162
|
+
if isinstance(message, AssistantMessage):
|
|
163
|
+
text.append(block.text)
|
|
164
|
+
if not partial: # else it already streamed in, token by token
|
|
165
|
+
emit("received", self.name, block.text, False)
|
|
166
|
+
elif isinstance(block, ThinkingBlock):
|
|
167
|
+
if not partial:
|
|
168
|
+
emit("thinking", "thinking", _clip(block.thinking), False)
|
|
169
|
+
elif isinstance(block, ToolUseBlock):
|
|
170
|
+
emit("tool use", block.name, _clip(json.dumps(block.input, indent=2)), False)
|
|
171
|
+
elif isinstance(block, ToolResultBlock):
|
|
172
|
+
kind = "error" if block.is_error else "tool result"
|
|
173
|
+
emit(kind, kind, _clip(block.content), False)
|
|
174
|
+
if isinstance(message, ResultMessage):
|
|
175
|
+
# ponytail: resuming the session is how follow-up asks keep context.
|
|
176
|
+
# Drop these two lines if you want every ask() stateless.
|
|
177
|
+
if message.session_id:
|
|
178
|
+
self.options.resume = message.session_id
|
|
179
|
+
if message.result:
|
|
180
|
+
return message.result
|
|
181
|
+
return "\n".join(text)
|
|
182
|
+
|
|
183
|
+
def __repr__(self):
|
|
184
|
+
return f"<Agent {self.name!r}>"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def create_agent_from_markdown(markdown_file_path, anthropic_api_key=None, **overrides):
|
|
188
|
+
"""Build an Agent from a markdown file. Extra kwargs go to ClaudeAgentOptions."""
|
|
189
|
+
path = Path(markdown_file_path)
|
|
190
|
+
meta, system_prompt = _parse_markdown(path.read_text(encoding="utf-8"))
|
|
191
|
+
|
|
192
|
+
env = dict(os.environ)
|
|
193
|
+
if anthropic_api_key:
|
|
194
|
+
env["ANTHROPIC_API_KEY"] = anthropic_api_key
|
|
195
|
+
elif "ANTHROPIC_API_KEY" not in env:
|
|
196
|
+
raise ValueError("no anthropic_api_key given and ANTHROPIC_API_KEY is unset")
|
|
197
|
+
|
|
198
|
+
opts = dict(
|
|
199
|
+
system_prompt=system_prompt,
|
|
200
|
+
env=env,
|
|
201
|
+
# ponytail: headless agents can't answer permission prompts. Pass
|
|
202
|
+
# permission_mode="acceptEdits" (or "plan") if bypass is too much.
|
|
203
|
+
permission_mode=meta.get("permission_mode", "bypassPermissions"),
|
|
204
|
+
# ponytail: always on rather than a flag — the extra pipe traffic is
|
|
205
|
+
# cheap and it's what makes ask_html fill in word by word.
|
|
206
|
+
include_partial_messages=True,
|
|
207
|
+
)
|
|
208
|
+
if "model" in meta:
|
|
209
|
+
opts["model"] = meta["model"]
|
|
210
|
+
if "tools" in meta:
|
|
211
|
+
opts["allowed_tools"] = [t.strip() for t in meta["tools"].split(",") if t.strip()]
|
|
212
|
+
opts.update(overrides)
|
|
213
|
+
|
|
214
|
+
return Agent(meta.get("name", path.stem), ClaudeAgentOptions(**opts))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
meta, body = _parse_markdown(
|
|
219
|
+
"---\nname: bob\ntools: Read, Grep\n---\nYou are bob.\n\n---\nnot frontmatter\n"
|
|
220
|
+
)
|
|
221
|
+
assert meta == {"name": "bob", "tools": "Read, Grep"}, meta
|
|
222
|
+
assert body == "You are bob.\n\n---\nnot frontmatter", repr(body)
|
|
223
|
+
|
|
224
|
+
meta, body = _parse_markdown("Just a prompt.")
|
|
225
|
+
assert meta == {} and body == "Just a prompt."
|
|
226
|
+
|
|
227
|
+
tmp = Path("_saf_selfcheck.md")
|
|
228
|
+
tmp.write_text("---\nname: bob\nmodel: claude-opus-5\ntools: Read\n---\nYou are bob.")
|
|
229
|
+
try:
|
|
230
|
+
a = create_agent_from_markdown(tmp, "sk-test")
|
|
231
|
+
assert a.name == "bob" and a.options.model == "claude-opus-5"
|
|
232
|
+
assert a.options.allowed_tools == ["Read"]
|
|
233
|
+
assert a.options.env["ANTHROPIC_API_KEY"] == "sk-test"
|
|
234
|
+
assert a.options.include_partial_messages is True
|
|
235
|
+
|
|
236
|
+
# token streaming: deltas redraw one card, the finished block must not
|
|
237
|
+
# append a second copy of the same text.
|
|
238
|
+
import claude_agent_sdk as _sdk
|
|
239
|
+
|
|
240
|
+
async def fake_query(prompt, options):
|
|
241
|
+
for chunk in ("Hel", "lo ", "there"):
|
|
242
|
+
yield _sdk.StreamEvent(uuid="u", session_id="s", event={
|
|
243
|
+
"type": "content_block_delta",
|
|
244
|
+
"delta": {"type": "text_delta", "text": chunk}})
|
|
245
|
+
yield _sdk.StreamEvent(uuid="u", session_id="s",
|
|
246
|
+
event={"type": "content_block_stop"})
|
|
247
|
+
yield AssistantMessage(content=[TextBlock("Hello there")], model="m")
|
|
248
|
+
yield ResultMessage(subtype="success", duration_ms=1, duration_api_ms=1,
|
|
249
|
+
is_error=False, num_turns=1, session_id="sess-1",
|
|
250
|
+
result="Hello there")
|
|
251
|
+
|
|
252
|
+
global query
|
|
253
|
+
query, seen = fake_query, []
|
|
254
|
+
out = _run(a.ask_async("hi", lambda *e: seen.append(e)))
|
|
255
|
+
assert out == "Hello there", out
|
|
256
|
+
assert [e[2] for e in seen] == ["Hel", "Hello ", "Hello there"], seen
|
|
257
|
+
assert all(e[3] for e in seen), "deltas must be replace=True"
|
|
258
|
+
assert a.options.resume == "sess-1"
|
|
259
|
+
finally:
|
|
260
|
+
tmp.unlink()
|
|
261
|
+
print("ok")
|