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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pip show:*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ *.egg-info/
@@ -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