mirror of
https://github.com/bleak-ai/gcontext.git
synced 2026-08-11 13:19:23 +02:00
feat: solve command visibility
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Workflows
|
||||
|
||||
A context-based workflow is a module with a fixed shape. It is a series of steps the agent executes with judgment, where every run leaves a persistent trace on disk. The workflow remembers what happened last run, accumulates knowledge, and gets better over time. A skill or prompt runs and forgets; a workflow holds state.
|
||||
A context-based workflow is a module with a fixed shape. Every workflow lives in `modules/` as a module, but not every module is a workflow: a module is any folder of files that holds accumulated knowledge on a topic; a workflow is a module that follows the specific structure defined here (frontmatter manifest, steps/, runs/). It is a series of steps the agent executes with judgment, where every run leaves a persistent trace on disk. The workflow remembers what happened last run, accumulates knowledge, and gets better over time. A skill or prompt runs and forgets; a workflow holds state.
|
||||
|
||||
This document is the template spec: the contract a workflow folder must follow to be distributable. The CLI (`gcontext add`), the site directory, and the authoring tooling all build against it. It is one standard for all workflows; there are no per-domain variants.
|
||||
|
||||
|
||||
+82
-17
@@ -154,18 +154,41 @@ class ConnectionTracker(Middleware):
|
||||
return await call_next(context)
|
||||
|
||||
async def on_list_resources(self, context, call_next):
|
||||
"""The resource list is the state folder, scanned live: every listable
|
||||
file at gcontext://<path>, so runtimes can offer them for attachment."""
|
||||
result = await call_next(context)
|
||||
for rel in fs.walk_files(PROJECT_DIR):
|
||||
mime = "text/markdown" if rel.endswith(".md") else "text/plain"
|
||||
result.append(Resource(uri=f"gcontext://{rel}", name=rel, mime_type=mime))
|
||||
"""Curated resource list: the agent entry point plus each module and
|
||||
connection. Every file stays readable via the gcontext://{path*}
|
||||
template; only the entry points appear as suggestions."""
|
||||
await call_next(context)
|
||||
result = []
|
||||
config = state.load_gcontext_yaml(PROJECT_DIR)
|
||||
agent_name = config.get("name", PROJECT_DIR.name)
|
||||
result.append(Resource(
|
||||
uri=f"agent://{agent_name}",
|
||||
name=agent_name,
|
||||
mime_type="text/markdown",
|
||||
))
|
||||
for name in state.discover_modules(PROJECT_DIR):
|
||||
result.append(Resource(
|
||||
uri=f"agent://{agent_name}/modules/{name}",
|
||||
name=f"modules / {name}",
|
||||
mime_type="text/markdown",
|
||||
))
|
||||
for name in state.load_connections(PROJECT_DIR):
|
||||
result.append(Resource(
|
||||
uri=f"agent://{agent_name}/connections/{name}",
|
||||
name=f"connections / {name}",
|
||||
mime_type="text/markdown",
|
||||
))
|
||||
return result
|
||||
|
||||
async def on_read_resource(self, context, call_next):
|
||||
from fastmcp.resources.base import ResourceResult
|
||||
uri = str(getattr(context.message, "uri", "?"))
|
||||
start = time.perf_counter()
|
||||
result = await call_next(context)
|
||||
text = _resolve_resource_uri(uri)
|
||||
if text is not None:
|
||||
result = ResourceResult(text)
|
||||
else:
|
||||
result = await call_next(context)
|
||||
record_event(_session_id(context), "resource", "resource", detail=uri,
|
||||
duration_ms=round((time.perf_counter() - start) * 1000))
|
||||
return result
|
||||
@@ -221,16 +244,58 @@ def load_instructions() -> tuple[int, int]:
|
||||
return len(base.splitlines()), len(text.splitlines())
|
||||
|
||||
|
||||
@mcp.resource("gcontext://{path*}",
|
||||
description=(_PROMPTS_DIR / "resources.md").read_text().strip())
|
||||
def state_resource(path: str) -> str:
|
||||
rel = path.rstrip("/")
|
||||
target, error = fs.resolve_path(PROJECT_DIR, rel)
|
||||
if error:
|
||||
return f"Error: {error}."
|
||||
if target.is_dir():
|
||||
return fs.list_dir(PROJECT_DIR, rel or ".")
|
||||
return fs.read_file(PROJECT_DIR, rel)
|
||||
def _resolve_resource_uri(uri: str) -> str | None:
|
||||
"""Resolve a resource URI to text content, or None if unrecognised."""
|
||||
if uri.startswith("agent://"):
|
||||
path = uri[len("agent://"):].rstrip("/")
|
||||
parts = path.split("/", 1)
|
||||
rel = parts[1] if len(parts) > 1 else ""
|
||||
if not rel:
|
||||
return _ask_resource()
|
||||
target, error = fs.resolve_path(PROJECT_DIR, rel)
|
||||
if error:
|
||||
return f"Error: {error}."
|
||||
if target.is_dir():
|
||||
index = target / "index.md"
|
||||
if index.is_file():
|
||||
return fs.read_file(PROJECT_DIR, f"{rel}/index.md")
|
||||
return fs.list_dir(PROJECT_DIR, rel)
|
||||
return fs.read_file(PROJECT_DIR, rel)
|
||||
if uri.startswith("gcontext://"):
|
||||
rel = uri[len("gcontext://"):].rstrip("/")
|
||||
target, error = fs.resolve_path(PROJECT_DIR, rel)
|
||||
if error:
|
||||
return f"Error: {error}."
|
||||
if target.is_dir():
|
||||
return fs.list_dir(PROJECT_DIR, rel or ".")
|
||||
return fs.read_file(PROJECT_DIR, rel)
|
||||
return None
|
||||
|
||||
|
||||
def _ask_resource() -> str:
|
||||
"""Build the 'ask' resource: agent.md plus a map of modules and connections."""
|
||||
config = state.load_gcontext_yaml(PROJECT_DIR)
|
||||
agent_name = config.get("name", PROJECT_DIR.name)
|
||||
parts = [f"# {agent_name}\n"]
|
||||
agent_md = PROJECT_DIR / "agent.md"
|
||||
if agent_md.exists():
|
||||
parts.append(agent_md.read_text().strip())
|
||||
parts.append("")
|
||||
modules = state.discover_modules(PROJECT_DIR)
|
||||
if modules:
|
||||
parts.append("## Modules")
|
||||
for name, manifest in modules.items():
|
||||
desc = f" - {manifest.description}" if manifest.description else ""
|
||||
parts.append(f"- {name}{desc}")
|
||||
parts.append("")
|
||||
connections = state.load_connections(PROJECT_DIR)
|
||||
if connections:
|
||||
parts.append("## Connections")
|
||||
for name, manifest in connections.items():
|
||||
desc = f" - {manifest.description}" if manifest.description else ""
|
||||
parts.append(f"- {name}{desc}")
|
||||
parts.append("")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# output_schema=None on every tool: with a schema, FastMCP wraps the string
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_commands_ledger_pipe(project):
|
||||
|
||||
def test_register_framework_prompts_setup():
|
||||
mcp = FastMCP("t")
|
||||
assert commands.register_framework_prompts(mcp) == 1
|
||||
assert commands.register_framework_prompts(mcp) == 2
|
||||
|
||||
async def go():
|
||||
async with Client(mcp) as c:
|
||||
|
||||
@@ -338,7 +338,8 @@ def test_state_files_are_resources(project):
|
||||
return listed, file[0].text, folder[0].text, archived[0].text, blocked[0].text
|
||||
|
||||
listed, file_text, folder_text, archived_text, blocked_text = asyncio.run(go())
|
||||
assert "gcontext://modules/m/index.md" in listed
|
||||
assert any(u.startswith("agent://") and u.endswith("/modules/m") for u in listed)
|
||||
assert any(u.startswith("agent://") and "modules" not in u and "connections" not in u for u in listed)
|
||||
assert not any("secrets.env" in u for u in listed)
|
||||
assert not any(u.startswith("gcontext://archive/") for u in listed)
|
||||
assert file_text == "topic notes"
|
||||
|
||||
Reference in New Issue
Block a user