agent-shared-context 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.
- agent_shared_context-0.1.0/.claude/settings.local.json +7 -0
- agent_shared_context-0.1.0/.gitignore +5 -0
- agent_shared_context-0.1.0/PKG-INFO +56 -0
- agent_shared_context-0.1.0/README.md +46 -0
- agent_shared_context-0.1.0/pyproject.toml +21 -0
- agent_shared_context-0.1.0/src/asc/__init__.py +0 -0
- agent_shared_context-0.1.0/src/asc/chat.py +98 -0
- agent_shared_context-0.1.0/src/asc/cli.py +68 -0
- agent_shared_context-0.1.0/src/asc/context.py +73 -0
- agent_shared_context-0.1.0/src/asc/dispatch.py +110 -0
- agent_shared_context-0.1.0/src/asc/register.py +32 -0
- agent_shared_context-0.1.0/src/asc/ui.py +70 -0
- agent_shared_context-0.1.0/uv.lock +829 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-shared-context
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Personal work orchestrator. One CLI, one main context, many agents.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: claude-agent-sdk
|
|
8
|
+
Requires-Dist: pyyaml
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# asc
|
|
12
|
+
|
|
13
|
+
Agent Shared Context. One CLI, one main context, many agents.
|
|
14
|
+
|
|
15
|
+
asc is a protocol for agents to share context. When an agent finishes work, it registers a zero-loss brief. Your main thread stays coherent across every tool, every agent, every project.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
uv tool install agent-shared-context
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
24
|
+
|
|
25
|
+
## The protocol
|
|
26
|
+
|
|
27
|
+
When you're done with a task, register a zero-loss brief:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
echo "<zero-loss-brief>" | asc register --author <name>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A zero-loss brief is **complete** (every decision, artifact, loose end), **concise** (dense, not long), and **concrete** (specific paths, names, states — actionable).
|
|
34
|
+
|
|
35
|
+
That's the protocol. Put it in your CLAUDE.md, your Cursor rules, your AGENTS.md. Any agent that can run a shell command can follow it.
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
asc where are we # talk to the main thread
|
|
41
|
+
asc run claude debug the migration issue # dispatch an agent
|
|
42
|
+
asc run --ro claude research k8s networking # read-only dispatch
|
|
43
|
+
echo "<brief>" | asc register --author gemini # register a handoff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
- **`asc <message>`** — talk to your main thread. Read-only. It knows everything that's been registered and synthesizes it for you.
|
|
49
|
+
- **`asc run <agent> <task>`** — dispatch an agent. It works, then registers a zero-loss brief when done.
|
|
50
|
+
- **`asc register --author <name>`** — the universal write API. Any agent, any surface, anywhere.
|
|
51
|
+
|
|
52
|
+
Context is stored as YAML day files in `~/.asc/days/`. Not per-project. Your entire work environment.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# asc
|
|
2
|
+
|
|
3
|
+
Agent Shared Context. One CLI, one main context, many agents.
|
|
4
|
+
|
|
5
|
+
asc is a protocol for agents to share context. When an agent finishes work, it registers a zero-loss brief. Your main thread stays coherent across every tool, every agent, every project.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
uv tool install agent-shared-context
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
14
|
+
|
|
15
|
+
## The protocol
|
|
16
|
+
|
|
17
|
+
When you're done with a task, register a zero-loss brief:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
echo "<zero-loss-brief>" | asc register --author <name>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A zero-loss brief is **complete** (every decision, artifact, loose end), **concise** (dense, not long), and **concrete** (specific paths, names, states — actionable).
|
|
24
|
+
|
|
25
|
+
That's the protocol. Put it in your CLAUDE.md, your Cursor rules, your AGENTS.md. Any agent that can run a shell command can follow it.
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
asc where are we # talk to the main thread
|
|
31
|
+
asc run claude debug the migration issue # dispatch an agent
|
|
32
|
+
asc run --ro claude research k8s networking # read-only dispatch
|
|
33
|
+
echo "<brief>" | asc register --author gemini # register a handoff
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
- **`asc <message>`** — talk to your main thread. Read-only. It knows everything that's been registered and synthesizes it for you.
|
|
39
|
+
- **`asc run <agent> <task>`** — dispatch an agent. It works, then registers a zero-loss brief when done.
|
|
40
|
+
- **`asc register --author <name>`** — the universal write API. Any agent, any surface, anywhere.
|
|
41
|
+
|
|
42
|
+
Context is stored as YAML day files in `~/.asc/days/`. Not per-project. Your entire work environment.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-shared-context"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Personal work orchestrator. One CLI, one main context, many agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"claude-agent-sdk",
|
|
14
|
+
"pyyaml",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
asc = "asc.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/asc"]
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Chat agent — the main thread interface."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
|
|
7
|
+
from claude_agent_sdk.types import StreamEvent, ToolUseBlock
|
|
8
|
+
|
|
9
|
+
from asc.context import ContextItem, append_item, load_recent_items
|
|
10
|
+
from asc.ui import format_items, print_step, tool_label, spin, CYAN, RESET
|
|
11
|
+
|
|
12
|
+
SYSTEM_PROMPT = """\
|
|
13
|
+
You are the main thread agent for asc — a personal work orchestrator.
|
|
14
|
+
|
|
15
|
+
You are the user's central context. You know what's been happening across all their work: \
|
|
16
|
+
dispatched agents, registrations, conversations. You synthesize it all and respond concisely.
|
|
17
|
+
|
|
18
|
+
When the user asks "where are we" or "what's going on" — you give them the real picture. \
|
|
19
|
+
Not a dump. A crisp, coherent status based on everything you know.
|
|
20
|
+
|
|
21
|
+
You speak directly. No fluff. You remember prior context and build on it.
|
|
22
|
+
|
|
23
|
+
## Current context
|
|
24
|
+
{context}"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _chat_async(message: str) -> None:
|
|
28
|
+
items = load_recent_items()
|
|
29
|
+
context = format_items(items)
|
|
30
|
+
system = SYSTEM_PROMPT.format(context=context)
|
|
31
|
+
|
|
32
|
+
append_item(ContextItem.now(author="user", type="dialogue", content=message))
|
|
33
|
+
|
|
34
|
+
options = ClaudeAgentOptions(
|
|
35
|
+
system_prompt=system,
|
|
36
|
+
allowed_tools=["Read", "Glob", "Grep", "WebSearch", "WebFetch"],
|
|
37
|
+
max_turns=10,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
full_text = ""
|
|
41
|
+
printed_len = 0
|
|
42
|
+
spinner_task = asyncio.create_task(spin())
|
|
43
|
+
spinning = True
|
|
44
|
+
|
|
45
|
+
def _stop_spinner():
|
|
46
|
+
nonlocal spinning
|
|
47
|
+
if not spinning:
|
|
48
|
+
return
|
|
49
|
+
spinning = False
|
|
50
|
+
spinner_task.cancel()
|
|
51
|
+
sys.stdout.write("\r\033[2K")
|
|
52
|
+
sys.stdout.flush()
|
|
53
|
+
|
|
54
|
+
def _start_spinner():
|
|
55
|
+
nonlocal spinning, spinner_task
|
|
56
|
+
if spinning:
|
|
57
|
+
return
|
|
58
|
+
spinning = True
|
|
59
|
+
spinner_task = asyncio.create_task(spin())
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
async for msg in query(prompt=message, options=options):
|
|
63
|
+
if isinstance(msg, StreamEvent):
|
|
64
|
+
event = msg.event
|
|
65
|
+
if event.get("type") == "content_block_delta":
|
|
66
|
+
delta = event.get("delta", {})
|
|
67
|
+
if delta.get("type") == "text_delta":
|
|
68
|
+
_stop_spinner()
|
|
69
|
+
new_text = delta.get("text", "")
|
|
70
|
+
full_text += new_text
|
|
71
|
+
print(f"{CYAN}{new_text}{RESET}", end="", flush=True)
|
|
72
|
+
printed_len += len(new_text)
|
|
73
|
+
elif isinstance(msg, AssistantMessage):
|
|
74
|
+
for block in msg.content:
|
|
75
|
+
if isinstance(block, ToolUseBlock):
|
|
76
|
+
_stop_spinner()
|
|
77
|
+
print_step(tool_label(block))
|
|
78
|
+
# Reset for next turn — text will be new
|
|
79
|
+
printed_len = 0
|
|
80
|
+
_start_spinner()
|
|
81
|
+
elif isinstance(block, TextBlock):
|
|
82
|
+
_stop_spinner()
|
|
83
|
+
if len(block.text) > printed_len:
|
|
84
|
+
remaining = block.text[printed_len:]
|
|
85
|
+
print(f"{CYAN}{remaining}{RESET}", end="", flush=True)
|
|
86
|
+
full_text = block.text
|
|
87
|
+
finally:
|
|
88
|
+
_stop_spinner()
|
|
89
|
+
|
|
90
|
+
if full_text:
|
|
91
|
+
print()
|
|
92
|
+
|
|
93
|
+
append_item(ContextItem.now(author="claude", type="dialogue", content=full_text.strip()))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def chat(message: str) -> None:
|
|
97
|
+
"""Send a message to the main thread agent, stream the response."""
|
|
98
|
+
asyncio.run(_chat_async(message))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""CLI entry point for asc."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from asc.chat import chat
|
|
6
|
+
from asc.register import register
|
|
7
|
+
from asc.ui import YELLOW, RESET
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
args = sys.argv[1:]
|
|
12
|
+
|
|
13
|
+
if not args:
|
|
14
|
+
print(f"{YELLOW}asc — personal work orchestrator{RESET}")
|
|
15
|
+
print()
|
|
16
|
+
print(" asc <message> talk to the main thread")
|
|
17
|
+
print(" asc register --author <name> register a zero-loss brief")
|
|
18
|
+
print(" asc run <agent> <task> dispatch an agent")
|
|
19
|
+
print(" asc run --ro <agent> <task> dispatch read-only agent")
|
|
20
|
+
sys.exit(0)
|
|
21
|
+
|
|
22
|
+
if args[0] == "register":
|
|
23
|
+
author = None
|
|
24
|
+
rest = args[1:]
|
|
25
|
+
|
|
26
|
+
if "--author" in rest:
|
|
27
|
+
idx = rest.index("--author")
|
|
28
|
+
if idx + 1 < len(rest):
|
|
29
|
+
author = rest[idx + 1]
|
|
30
|
+
rest = rest[:idx] + rest[idx + 2:]
|
|
31
|
+
else:
|
|
32
|
+
print(f"{YELLOW}error: --author requires a name{RESET}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
text = " ".join(rest) if rest else None
|
|
36
|
+
register(author=author, text=text)
|
|
37
|
+
|
|
38
|
+
elif args[0] == "run":
|
|
39
|
+
rest = args[1:]
|
|
40
|
+
read_only = False
|
|
41
|
+
|
|
42
|
+
if "--ro" in rest:
|
|
43
|
+
read_only = True
|
|
44
|
+
rest.remove("--ro")
|
|
45
|
+
|
|
46
|
+
if len(rest) < 2:
|
|
47
|
+
print(f"{YELLOW}usage: asc run <agent> <task>{RESET}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
agent = rest[0]
|
|
51
|
+
task = " ".join(rest[1:])
|
|
52
|
+
|
|
53
|
+
from asc.dispatch import dispatch
|
|
54
|
+
try:
|
|
55
|
+
dispatch(agent=agent, task=task, read_only=read_only)
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
print(f"\n{YELLOW}dispatch interrupted.{RESET}")
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
message = " ".join(args)
|
|
61
|
+
try:
|
|
62
|
+
chat(message)
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
print()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Context item data model and day file management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, asdict, field
|
|
5
|
+
from datetime import datetime, date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
ASC_DIR = Path.home() / ".asc"
|
|
11
|
+
DAYS_DIR = ASC_DIR / "days"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ContextItem:
|
|
16
|
+
author: str
|
|
17
|
+
date: str
|
|
18
|
+
type: str
|
|
19
|
+
content: str
|
|
20
|
+
cwd: str | None = field(default=None)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def now(author: str, type: str, content: str, cwd: str | None = None) -> "ContextItem":
|
|
24
|
+
return ContextItem(
|
|
25
|
+
author=author,
|
|
26
|
+
date=datetime.now().astimezone().isoformat(),
|
|
27
|
+
type=type,
|
|
28
|
+
content=content,
|
|
29
|
+
cwd=cwd or os.getcwd(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ensure_dirs():
|
|
34
|
+
DAYS_DIR.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _today_file() -> Path:
|
|
38
|
+
return DAYS_DIR / f"{date.today().isoformat()}.yaml"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def append_item(item: ContextItem) -> None:
|
|
42
|
+
"""Append a context item to today's day file."""
|
|
43
|
+
_ensure_dirs()
|
|
44
|
+
path = _today_file()
|
|
45
|
+
|
|
46
|
+
items = load_day(path)
|
|
47
|
+
items.append(asdict(item))
|
|
48
|
+
|
|
49
|
+
with open(path, "w") as f:
|
|
50
|
+
yaml.dump(items, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_day(path: Path) -> list[dict]:
|
|
54
|
+
"""Load all items from a day file."""
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return []
|
|
57
|
+
with open(path) as f:
|
|
58
|
+
data = yaml.safe_load(f)
|
|
59
|
+
return data if isinstance(data, list) else []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_recent_items() -> list[dict]:
|
|
63
|
+
"""Load raw items from today and yesterday."""
|
|
64
|
+
_ensure_dirs()
|
|
65
|
+
today = date.today()
|
|
66
|
+
yesterday = date.fromordinal(today.toordinal() - 1)
|
|
67
|
+
|
|
68
|
+
items = []
|
|
69
|
+
for d in [yesterday, today]:
|
|
70
|
+
path = DAYS_DIR / f"{d.isoformat()}.yaml"
|
|
71
|
+
items.extend(load_day(path))
|
|
72
|
+
|
|
73
|
+
return items
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Dispatch — run agents with context and step printing."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from claude_agent_sdk import (
|
|
7
|
+
ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, TextBlock,
|
|
8
|
+
tool, create_sdk_mcp_server,
|
|
9
|
+
)
|
|
10
|
+
from claude_agent_sdk.types import StreamEvent, ToolUseBlock
|
|
11
|
+
|
|
12
|
+
from asc.context import ContextItem, append_item, load_recent_items
|
|
13
|
+
from asc.ui import format_items, print_step, tool_label, CYAN, YELLOW, RESET
|
|
14
|
+
|
|
15
|
+
READ_ONLY_TOOLS = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"]
|
|
16
|
+
|
|
17
|
+
ZLB_DEFINITION = """\
|
|
18
|
+
A zero-loss brief follows three rules:
|
|
19
|
+
- Complete — every decision, every artifact, every loose end
|
|
20
|
+
- Concise — dense, not long
|
|
21
|
+
- Concrete — specific paths, names, states. Actionable."""
|
|
22
|
+
|
|
23
|
+
DISPATCH_SYSTEM_PROMPT = """\
|
|
24
|
+
You are a dispatched agent working on a specific task.
|
|
25
|
+
|
|
26
|
+
## Your task
|
|
27
|
+
{task}
|
|
28
|
+
|
|
29
|
+
## Context from the main thread
|
|
30
|
+
{context}
|
|
31
|
+
|
|
32
|
+
## When you're done
|
|
33
|
+
Write a zero-loss brief and register it using the asc_register tool.
|
|
34
|
+
|
|
35
|
+
{zlb_definition}
|
|
36
|
+
|
|
37
|
+
Do your work, then register. Every time."""
|
|
38
|
+
|
|
39
|
+
READ_ONLY_ADDENDUM = """
|
|
40
|
+
|
|
41
|
+
## Read-only mode
|
|
42
|
+
You are in read-only mode. Do NOT create, edit, or delete any files. \
|
|
43
|
+
You may only read files, search, and browse the web."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _make_register_tool(agent: str):
|
|
47
|
+
@tool("asc_register", "Register a zero-loss brief to the main context.", {"brief": str})
|
|
48
|
+
async def asc_register(args):
|
|
49
|
+
append_item(ContextItem.now(
|
|
50
|
+
author=agent,
|
|
51
|
+
type="registration",
|
|
52
|
+
content=args["brief"],
|
|
53
|
+
))
|
|
54
|
+
return {"content": [{"type": "text", "text": "registered."}]}
|
|
55
|
+
|
|
56
|
+
return asc_register
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _dispatch_async(agent: str, task: str, read_only: bool) -> None:
|
|
60
|
+
items = load_recent_items()
|
|
61
|
+
context = format_items(items)
|
|
62
|
+
|
|
63
|
+
system = DISPATCH_SYSTEM_PROMPT.format(
|
|
64
|
+
task=task,
|
|
65
|
+
context=context,
|
|
66
|
+
agent=agent,
|
|
67
|
+
zlb_definition=ZLB_DEFINITION,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
register_tool = _make_register_tool(agent)
|
|
71
|
+
asc_server = create_sdk_mcp_server("asc", tools=[register_tool])
|
|
72
|
+
|
|
73
|
+
if read_only:
|
|
74
|
+
system += READ_ONLY_ADDENDUM
|
|
75
|
+
options = ClaudeAgentOptions(
|
|
76
|
+
system_prompt=system,
|
|
77
|
+
cwd=os.getcwd(),
|
|
78
|
+
allowed_tools=[*READ_ONLY_TOOLS, "mcp__asc__asc_register"],
|
|
79
|
+
mcp_servers={"asc": asc_server},
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
options = ClaudeAgentOptions(
|
|
83
|
+
system_prompt=system,
|
|
84
|
+
cwd=os.getcwd(),
|
|
85
|
+
permission_mode="bypassPermissions",
|
|
86
|
+
mcp_servers={"asc": asc_server},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print_step(f"Starting: {task}", prefix=agent, color=YELLOW)
|
|
90
|
+
|
|
91
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
92
|
+
await client.query(task)
|
|
93
|
+
|
|
94
|
+
async for msg in client.receive_response():
|
|
95
|
+
if isinstance(msg, AssistantMessage):
|
|
96
|
+
for block in msg.content:
|
|
97
|
+
if isinstance(block, TextBlock) and block.text.strip():
|
|
98
|
+
print(f"{CYAN}{block.text}{RESET}", flush=True)
|
|
99
|
+
elif isinstance(block, ToolUseBlock):
|
|
100
|
+
print_step(tool_label(block), prefix=agent)
|
|
101
|
+
|
|
102
|
+
elif isinstance(msg, StreamEvent):
|
|
103
|
+
pass # streaming deltas — agent text already handled above
|
|
104
|
+
|
|
105
|
+
print_step("Done.", prefix=agent, color=YELLOW)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def dispatch(agent: str, task: str, read_only: bool = False) -> None:
|
|
109
|
+
"""Dispatch an agent to work on a task."""
|
|
110
|
+
asyncio.run(_dispatch_async(agent, task, read_only))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Register command — universal write API to the main context."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from asc.context import ContextItem, append_item
|
|
6
|
+
from asc.ui import YELLOW, MAGENTA, RESET
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(author: str | None = None, text: str | None = None) -> None:
|
|
10
|
+
"""Register a zero-loss brief to the main context.
|
|
11
|
+
|
|
12
|
+
Canonical usage: echo "<zero-loss-brief>" | asc register --author gemini
|
|
13
|
+
"""
|
|
14
|
+
if text:
|
|
15
|
+
content = text
|
|
16
|
+
elif not sys.stdin.isatty():
|
|
17
|
+
content = sys.stdin.read().strip()
|
|
18
|
+
else:
|
|
19
|
+
print(f"{YELLOW}usage: echo '<zero-loss-brief>' | asc register --author <name>{RESET}")
|
|
20
|
+
print(f"{YELLOW} asc register --author <name> <text>{RESET}")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
if not content:
|
|
24
|
+
print(f"{YELLOW}error: empty registration{RESET}")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
append_item(ContextItem.now(
|
|
28
|
+
author=author or "agent",
|
|
29
|
+
type="registration",
|
|
30
|
+
content=content,
|
|
31
|
+
))
|
|
32
|
+
print(f"{MAGENTA}registered.{RESET}")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared UI helpers — spinners, step printing, tool labels."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from claude_agent_sdk.types import ToolUseBlock
|
|
7
|
+
|
|
8
|
+
# ANSI color constants
|
|
9
|
+
CYAN = "\033[36m"
|
|
10
|
+
YELLOW = "\033[33m"
|
|
11
|
+
MAGENTA = "\033[35m"
|
|
12
|
+
DIM = "\033[2m"
|
|
13
|
+
RESET = "\033[0m"
|
|
14
|
+
|
|
15
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_items(items: list[dict]) -> str:
|
|
19
|
+
if not items:
|
|
20
|
+
return "(no activity yet)"
|
|
21
|
+
|
|
22
|
+
lines = []
|
|
23
|
+
for item in items:
|
|
24
|
+
author = item.get("author", "unknown")
|
|
25
|
+
date = item.get("date", "")
|
|
26
|
+
type_ = item.get("type", "")
|
|
27
|
+
content = item.get("content", "")
|
|
28
|
+
cwd = item.get("cwd", "")
|
|
29
|
+
prefix = f"[{date}] ({type_}) {author}"
|
|
30
|
+
if cwd:
|
|
31
|
+
prefix += f" [{cwd}]"
|
|
32
|
+
lines.append(f"{prefix}: {content}")
|
|
33
|
+
return "\n".join(lines)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def print_step(msg: str, prefix: str | None = None, color: str = DIM):
|
|
37
|
+
label = f"[{prefix}] {msg}" if prefix else f" {msg}"
|
|
38
|
+
print(f"{color}{label}{RESET}", flush=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tool_label(block: ToolUseBlock) -> str:
|
|
42
|
+
"""Build a short human-readable label for a tool call."""
|
|
43
|
+
name = block.name
|
|
44
|
+
inp = block.input or {}
|
|
45
|
+
|
|
46
|
+
if name == "WebSearch":
|
|
47
|
+
return f"Searching: {inp.get('query', '')[:80]}"
|
|
48
|
+
if name == "WebFetch":
|
|
49
|
+
return f"Fetching: {inp.get('url', '')[:80]}"
|
|
50
|
+
if name == "Bash":
|
|
51
|
+
return f"Running: {inp.get('command', '')[:80]}"
|
|
52
|
+
if name in ("Read", "Glob", "Grep"):
|
|
53
|
+
target = inp.get("file_path") or inp.get("pattern") or inp.get("path", "")
|
|
54
|
+
return f"{name}: {target[:80]}"
|
|
55
|
+
if name in ("Edit", "Write"):
|
|
56
|
+
return f"{name}: {inp.get('file_path', '')}"
|
|
57
|
+
return name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def spin():
|
|
61
|
+
i = 0
|
|
62
|
+
try:
|
|
63
|
+
while True:
|
|
64
|
+
frame = SPINNER_FRAMES[i % len(SPINNER_FRAMES)]
|
|
65
|
+
sys.stdout.write(f"\r{CYAN}{frame}{RESET} ")
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
i += 1
|
|
68
|
+
await asyncio.sleep(0.08)
|
|
69
|
+
except asyncio.CancelledError:
|
|
70
|
+
pass
|