generated from coulomb/repo-seed
Layered context memory revision 0
This commit is contained in:
@@ -57,6 +57,18 @@ from markitect_tool.generation import (
|
||||
run_generation_plan,
|
||||
)
|
||||
from markitect_tool.literate import tangle_markdown, weave_markdown, write_tangle_files
|
||||
from markitect_tool.memory import (
|
||||
ContextBudget,
|
||||
ContextPackageError,
|
||||
LocalContextPackageRegistry,
|
||||
MemoryNamespace,
|
||||
activate_context_package,
|
||||
create_context_package_from_index,
|
||||
create_context_package_from_manifest,
|
||||
create_context_package_from_sources,
|
||||
explain_context_package,
|
||||
refresh_context_package,
|
||||
)
|
||||
from markitect_tool.ops import IncludeError, compose_files, resolve_includes, transform_markdown
|
||||
from markitect_tool.processor import ProcessorContext, run_fenced_processors
|
||||
from markitect_tool.policy import (
|
||||
@@ -1392,6 +1404,373 @@ def search(
|
||||
_emit_search_results(data, output_format)
|
||||
|
||||
|
||||
@main.group("context")
|
||||
def context_group() -> None:
|
||||
"""Pack and activate agent working-memory context."""
|
||||
|
||||
|
||||
@context_group.command("pack")
|
||||
@click.argument("query_or_manifest")
|
||||
@click.option(
|
||||
"--source",
|
||||
"sources",
|
||||
multiple=True,
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Markdown source file to query directly. May be repeated.",
|
||||
)
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for relative paths and the local context registry.",
|
||||
)
|
||||
@click.option(
|
||||
"--index-path",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="SQLite index path. Defaults to .markitect/cache/index.sqlite3 under root.",
|
||||
)
|
||||
@click.option("--search", is_flag=True, help="Treat the argument as an FTS search query.")
|
||||
@click.option("--path", "paths", multiple=True, help="Restrict indexed query/search to relative paths.")
|
||||
@click.option(
|
||||
"--engine",
|
||||
type=click.Choice(["selector", "jsonpath"], case_sensitive=False),
|
||||
default="selector",
|
||||
show_default=True,
|
||||
help="Query engine for selector packages.",
|
||||
)
|
||||
@click.option("--limit", type=int, default=20, show_default=True, help="Maximum FTS search matches.")
|
||||
@click.option("--title", help="Package title.")
|
||||
@click.option("--intent", help="Package intent statement.")
|
||||
@click.option("--project", help="Project namespace.")
|
||||
@click.option("--user", "user_ns", help="User namespace.")
|
||||
@click.option("--agent", help="Agent namespace.")
|
||||
@click.option("--thread", help="Thread namespace.")
|
||||
@click.option("--task", help="Task namespace.")
|
||||
@click.option("--namespace", "namespace_pairs", multiple=True, metavar="KEY=VALUE", help="Custom namespace key.")
|
||||
@click.option("--max-items", type=int, help="Maximum context items to include.")
|
||||
@click.option("--max-tokens", type=int, help="Approximate maximum package tokens.")
|
||||
@click.option("--reserve-tokens", type=int, default=0, show_default=True, help="Token reserve inside max tokens.")
|
||||
@click.option(
|
||||
"--policy",
|
||||
"policy_file",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Local label policy file used to filter package items.",
|
||||
)
|
||||
@click.option("--subject", default="anonymous", show_default=True, help="Policy subject id.")
|
||||
@click.option(
|
||||
"--policy-mode",
|
||||
type=click.Choice(["off", "audit", "enforce"], case_sensitive=False),
|
||||
help="Override policy mode while packing.",
|
||||
)
|
||||
@click.option("--action", default="package", show_default=True, help="Policy action used while packing.")
|
||||
@click.option("--output", type=click.Path(dir_okay=False, path_type=Path), help="Write package to this file.")
|
||||
@click.option("--no-save", is_flag=True, help="Do not save to the local context registry.")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def context_pack(
|
||||
query_or_manifest: str,
|
||||
sources: tuple[Path, ...],
|
||||
root: Path,
|
||||
index_path: Path | None,
|
||||
search: bool,
|
||||
paths: tuple[str, ...],
|
||||
engine: str,
|
||||
limit: int,
|
||||
title: str | None,
|
||||
intent: str | None,
|
||||
project: str | None,
|
||||
user_ns: str | None,
|
||||
agent: str | None,
|
||||
thread: str | None,
|
||||
task: str | None,
|
||||
namespace_pairs: tuple[str, ...],
|
||||
max_items: int | None,
|
||||
max_tokens: int | None,
|
||||
reserve_tokens: int,
|
||||
policy_file: Path | None,
|
||||
subject: str,
|
||||
policy_mode: str | None,
|
||||
action: str,
|
||||
output: Path | None,
|
||||
no_save: bool,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Create an inspectable context package from a query, search, or manifest."""
|
||||
|
||||
try:
|
||||
namespace = _memory_namespace(project, user_ns, agent, thread, task, namespace_pairs)
|
||||
budget = ContextBudget(max_tokens=max_tokens, max_items=max_items, reserve_tokens=reserve_tokens)
|
||||
gateway = _load_policy_gateway(policy_file, policy_mode)
|
||||
manifest = Path(query_or_manifest)
|
||||
if manifest.exists() and manifest.is_file() and not sources:
|
||||
package = create_context_package_from_manifest(
|
||||
manifest,
|
||||
root=root,
|
||||
budget=budget,
|
||||
policy_gateway=gateway,
|
||||
subject=subject,
|
||||
action=action,
|
||||
)
|
||||
elif sources:
|
||||
package = create_context_package_from_sources(
|
||||
query_or_manifest,
|
||||
list(sources),
|
||||
root=root,
|
||||
engine=engine,
|
||||
title=title,
|
||||
intent=intent,
|
||||
namespace=namespace,
|
||||
budget=budget,
|
||||
policy_gateway=gateway,
|
||||
subject=subject,
|
||||
action=action,
|
||||
)
|
||||
else:
|
||||
package = create_context_package_from_index(
|
||||
query_or_manifest,
|
||||
root=root,
|
||||
index_path=index_path,
|
||||
engine=engine,
|
||||
paths=list(paths) or None,
|
||||
search=search,
|
||||
limit=limit,
|
||||
title=title,
|
||||
intent=intent,
|
||||
namespace=namespace,
|
||||
budget=budget,
|
||||
policy_gateway=gateway,
|
||||
subject=subject,
|
||||
action=action,
|
||||
)
|
||||
except (ContextPackageError, InvalidQueryError, ValueError) as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
registry_path = None
|
||||
if not no_save:
|
||||
registry_path = LocalContextPackageRegistry(root).save(package)
|
||||
if output:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(yaml.safe_dump(package.to_dict(), sort_keys=False), encoding="utf-8")
|
||||
_emit_context_package(
|
||||
package.to_dict()
|
||||
| {
|
||||
"registry_path": str(registry_path) if registry_path else None,
|
||||
"output_path": str(output) if output else None,
|
||||
},
|
||||
output_format,
|
||||
)
|
||||
|
||||
|
||||
@context_group.command("activate")
|
||||
@click.argument("package")
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for the local context registry.",
|
||||
)
|
||||
@click.option("--target", default="default", show_default=True, help="Thread/workspace activation target.")
|
||||
@click.option("--max-items", type=int, help="Maximum context items to activate.")
|
||||
@click.option("--max-tokens", type=int, help="Approximate maximum activation tokens.")
|
||||
@click.option("--reserve-tokens", type=int, default=0, show_default=True, help="Token reserve inside max tokens.")
|
||||
@click.option(
|
||||
"--policy",
|
||||
"policy_file",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Local label policy file used to re-check items before activation.",
|
||||
)
|
||||
@click.option("--subject", default="anonymous", show_default=True, help="Policy subject id.")
|
||||
@click.option(
|
||||
"--policy-mode",
|
||||
type=click.Choice(["off", "audit", "enforce"], case_sensitive=False),
|
||||
help="Override policy mode while activating.",
|
||||
)
|
||||
@click.option("--action", default="read", show_default=True, help="Policy action used while activating.")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["markdown", "json", "yaml", "text"], case_sensitive=False),
|
||||
default="markdown",
|
||||
show_default=True,
|
||||
)
|
||||
def context_activate(
|
||||
package: str,
|
||||
root: Path,
|
||||
target: str,
|
||||
max_items: int | None,
|
||||
max_tokens: int | None,
|
||||
reserve_tokens: int,
|
||||
policy_file: Path | None,
|
||||
subject: str,
|
||||
policy_mode: str | None,
|
||||
action: str,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Activate a saved context package as Markdown working memory."""
|
||||
|
||||
try:
|
||||
registry = LocalContextPackageRegistry(root)
|
||||
loaded = registry.load(package)
|
||||
activation = activate_context_package(
|
||||
loaded,
|
||||
target=target,
|
||||
policy_gateway=_load_policy_gateway(policy_file, policy_mode),
|
||||
subject=subject,
|
||||
action=action,
|
||||
budget=ContextBudget(max_tokens=max_tokens, max_items=max_items, reserve_tokens=reserve_tokens),
|
||||
)
|
||||
activation_path = registry.save_activation(activation)
|
||||
registry.save(loaded.with_activation_state("active"))
|
||||
except (ContextPackageError, ValueError) as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_context_activation(
|
||||
activation.to_dict() | {"activation_path": str(activation_path)},
|
||||
output_format,
|
||||
)
|
||||
|
||||
|
||||
@context_group.command("deactivate")
|
||||
@click.argument("activation_id")
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for the local context registry.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def context_deactivate(activation_id: str, root: Path, output_format: str) -> None:
|
||||
"""Deactivate a recorded context activation."""
|
||||
|
||||
try:
|
||||
registry = LocalContextPackageRegistry(root)
|
||||
registry.deactivate(activation_id)
|
||||
activation = registry.load_activation(activation_id)
|
||||
except ContextPackageError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_context_activation(activation.to_dict(), output_format)
|
||||
|
||||
|
||||
@context_group.command("explain")
|
||||
@click.argument("package")
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for the local context registry.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def context_explain(package: str, root: Path, output_format: str) -> None:
|
||||
"""Explain context package contents, retrieval, budget, and policy metadata."""
|
||||
|
||||
try:
|
||||
loaded = LocalContextPackageRegistry(root).load(package)
|
||||
except ContextPackageError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_context_explain(explain_context_package(loaded), output_format)
|
||||
|
||||
|
||||
@context_group.command("refresh")
|
||||
@click.argument("package")
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for the local context registry.",
|
||||
)
|
||||
@click.option(
|
||||
"--policy",
|
||||
"policy_file",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Local label policy file used to filter refreshed package items.",
|
||||
)
|
||||
@click.option("--subject", default="anonymous", show_default=True, help="Policy subject id.")
|
||||
@click.option(
|
||||
"--policy-mode",
|
||||
type=click.Choice(["off", "audit", "enforce"], case_sensitive=False),
|
||||
help="Override policy mode while refreshing.",
|
||||
)
|
||||
@click.option("--action", default="package", show_default=True, help="Policy action used while refreshing.")
|
||||
@click.option("--no-save", is_flag=True, help="Do not save the refreshed package.")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def context_refresh(
|
||||
package: str,
|
||||
root: Path,
|
||||
policy_file: Path | None,
|
||||
subject: str,
|
||||
policy_mode: str | None,
|
||||
action: str,
|
||||
no_save: bool,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Refresh a package by re-running its retrieval recipes."""
|
||||
|
||||
try:
|
||||
registry = LocalContextPackageRegistry(root)
|
||||
loaded = registry.load(package)
|
||||
refreshed = refresh_context_package(
|
||||
loaded,
|
||||
policy_gateway=_load_policy_gateway(policy_file, policy_mode),
|
||||
subject=subject,
|
||||
action=action,
|
||||
)
|
||||
registry_path = None if no_save else registry.save(refreshed)
|
||||
except (ContextPackageError, ValueError) as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_context_package(
|
||||
refreshed.to_dict() | {"registry_path": str(registry_path) if registry_path else None},
|
||||
output_format,
|
||||
)
|
||||
|
||||
|
||||
@context_group.command("list")
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for the local context registry.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def context_list(root: Path, output_format: str) -> None:
|
||||
"""List locally saved context packages."""
|
||||
|
||||
packages = [package.to_dict() for package in LocalContextPackageRegistry(root).list()]
|
||||
_emit_context_package_list({"count": len(packages), "packages": packages}, output_format)
|
||||
|
||||
|
||||
@main.group()
|
||||
def workflow() -> None:
|
||||
"""Inspect, plan, and run declarative Markdown workflows."""
|
||||
@@ -1809,6 +2188,25 @@ def _load_policy_gateway(
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
|
||||
|
||||
def _memory_namespace(
|
||||
project: str | None,
|
||||
user: str | None,
|
||||
agent: str | None,
|
||||
thread: str | None,
|
||||
task: str | None,
|
||||
namespace_pairs: tuple[str, ...],
|
||||
) -> MemoryNamespace:
|
||||
custom = _parse_key_value_options(namespace_pairs)
|
||||
return MemoryNamespace(
|
||||
project=project,
|
||||
user=user,
|
||||
agent=agent,
|
||||
thread=thread,
|
||||
task=task,
|
||||
custom=custom,
|
||||
)
|
||||
|
||||
|
||||
def _emit_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
@@ -2084,6 +2482,83 @@ def _emit_search_results(data: dict, output_format: str) -> None:
|
||||
click.echo(f"! [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def _emit_context_package(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo(f"package: {data['id']}")
|
||||
click.echo(f"title: {data.get('title', '')}")
|
||||
click.echo(f"items: {len(data.get('items', []))}")
|
||||
click.echo(f"token_estimate: {data.get('token_estimate', 0)}")
|
||||
if data.get("registry_path"):
|
||||
click.echo(f"registry_path: {data['registry_path']}")
|
||||
if data.get("output_path"):
|
||||
click.echo(f"output_path: {data['output_path']}")
|
||||
if data.get("policy", {}).get("summary"):
|
||||
_emit_policy_summary(data["policy"]["summary"])
|
||||
for summary in data.get("summaries", []):
|
||||
click.echo(f"- {summary['name']}: {summary['text']}")
|
||||
|
||||
|
||||
def _emit_context_activation(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
elif output_format == "markdown":
|
||||
click.echo(data.get("content", ""), nl=False)
|
||||
else:
|
||||
click.echo(f"activation: {data['id']}")
|
||||
click.echo(f"status: {data.get('status')}")
|
||||
click.echo(f"package: {data.get('package_id')}")
|
||||
click.echo(f"items: {len(data.get('items', []))}")
|
||||
click.echo(f"token_estimate: {data.get('token_estimate', 0)}")
|
||||
if data.get("activation_path"):
|
||||
click.echo(f"activation_path: {data['activation_path']}")
|
||||
if data.get("policy", {}).get("summary"):
|
||||
_emit_policy_summary(data["policy"]["summary"])
|
||||
for diagnostic in data.get("diagnostics", []):
|
||||
click.echo(f"! [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def _emit_context_explain(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo(f"package: {data['id']}")
|
||||
click.echo(f"title: {data.get('title', '')}")
|
||||
click.echo(f"intent: {data.get('intent', '')}")
|
||||
click.echo(f"activation_state: {data.get('activation_state', 'inactive')}")
|
||||
click.echo(f"items: {data.get('items', 0)}")
|
||||
click.echo(f"token_estimate: {data.get('token_estimate', 0)}")
|
||||
if data.get("namespace"):
|
||||
click.echo(f"namespace: {data['namespace']}")
|
||||
if data.get("retrieval_recipes"):
|
||||
click.echo("retrieval_recipes:")
|
||||
for recipe in data["retrieval_recipes"]:
|
||||
click.echo(f"- {recipe.get('kind')} {recipe.get('query')}")
|
||||
if data.get("sources"):
|
||||
click.echo("sources:")
|
||||
for source in data["sources"]:
|
||||
span = f":{source.get('line_start')}" if source.get("line_start") else ""
|
||||
click.echo(f"- {source.get('path')}{span} {source.get('unit_kind', '')}")
|
||||
|
||||
|
||||
def _emit_context_package_list(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo(f"packages: {data.get('count', 0)}")
|
||||
for package in data.get("packages", []):
|
||||
click.echo(f"- {package['id']} {package.get('title', '')}")
|
||||
|
||||
|
||||
def _emit_policy_summary(policy_data: dict) -> None:
|
||||
click.echo(
|
||||
"policy: "
|
||||
|
||||
Reference in New Issue
Block a user