feat: solve command visibility

This commit is contained in:
bernatsampera
2026-08-09 18:19:07 +02:00
parent 87d8575829
commit 07c8b76906
4 changed files with 86 additions and 20 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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:
+2 -1
View File
@@ -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"