a2a-source-edit: write agent.py

This commit is contained in:
a2a-cloud
2026-07-13 12:16:01 +00:00
parent f129021c58
commit 08377840a0

478
agent.py
View File

@@ -1,189 +1,383 @@
"""customer-integration-engineer agent. """Deterministic release validation agent for customer-integration-engineer.
Starter stack: This agent validates a customer-integration-engineer release without editing the
- DeepAgents for tool-calling orchestration validated source. It is intentionally local-logic only: no LLM credentials,
- Caller-provided LLM credentials via ctx.llm provider keys, code-editor-agent delegation, or cosmetic mutation paths.
- A tiny model-call middleware hook you can replace with tracing,
routing, rate limits, or policy checks
""" """
from __future__ import annotations from __future__ import annotations
import json import json
from pathlib import Path import re
from typing import Any from typing import Any
from urllib.parse import urlparse
from pydantic import BaseModel from pydantic import BaseModel, Field
import a2a_pack as a2a import a2a_pack as a2a
from a2a_pack import ( from a2a_pack import (
A2AAgent, A2AAgent,
LLMProvisioning, NoAuth,
{{ auth_type }},
Pricing, Pricing,
Resources,
RunContext, RunContext,
WorkspaceAccess, WorkspaceAccess,
WorkspaceMode, WorkspaceMode,
) )
from a2a_pack.context import LLMCreds
EXPECTED_AGENT_NAME = "customer-integration-engineer"
EXPECTED_LIVE_VERSION = "0.1.5"
EXPECTED_PUBLIC = False
EXPECTED_WORKSPACE_MODES = {"read_only", "read_write_overlay"}
EXPECTED_SKILL_COUNT = 5
MAX_ALLOWED_IMPROVEMENT_ITERATIONS = 0
SECRET_KEY_RE = re.compile(r"(api[_-]?key|token|secret|password|authorization|bearer)", re.I)
SECRET_VALUE_RE = re.compile(
r"(?i)(sk-[A-Za-z0-9_-]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|gh[pousr]_[A-Za-z0-9_]{20,}|bearer\s+[A-Za-z0-9._-]{16,})"
)
class CustomerIntegrationEngineerConfig(BaseModel): class CustomerIntegrationEngineerConfig(BaseModel):
pass """Static release-validation policy."""
target_agent_name: str = EXPECTED_AGENT_NAME
target_version: str = EXPECTED_LIVE_VERSION
required_skill_count: int = EXPECTED_SKILL_COUNT
SYSTEM_PROMPT = """\ class CardValidationInput(BaseModel):
You are a compact tool-calling agent. """Bounded card details copied from the live Agent Card or registry view."""
Use the text_stats tool when the user asks about text, counts, summaries, card: dict[str, Any] = Field(description="Live Agent Card JSON for the target release.")
or anything where exact length/word numbers would help. Mention tool results expected_version: str = Field(default=EXPECTED_LIVE_VERSION, max_length=32)
briefly instead of dumping raw JSON. expected_skill_count: int = Field(default=EXPECTED_SKILL_COUNT, ge=1, le=20)
""" require_private: bool = True
RUNTIME_SKILLS_DIR = "customer-integration-engineer/.deepagents/skills/"
DEEPAGENTS_RECURSION_LIMIT = 500
class CustomerIntegrationEngineer(A2AAgent[CustomerIntegrationEngineerConfig, {{ auth_type }}]): class SmokeCallInput(BaseModel):
name = "customer-integration-engineer" """Structured result from an Agent Studio delegated live smoke invocation."""
description = "Validates the customer-integration-engineer release end to end, preserving existing source unless critical defects remain."
skill_name: str = Field(min_length=1, max_length=80)
input_args: dict[str, Any] = Field(default_factory=dict)
result: dict[str, Any] = Field(default_factory=dict)
status_code: int = Field(default=200, ge=100, le=599)
used_workspace_delegation: bool = True
delegated_workspace_mode: str = Field(default="read_only", max_length=64)
class ReviewerFinding(BaseModel):
severity: str = Field(description="critical, warning, info, or pass", max_length=32)
area: str = Field(max_length=120)
message: str = Field(max_length=1000)
class CustomerIntegrationEngineer(A2AAgent[CustomerIntegrationEngineerConfig, NoAuth]):
name = EXPECTED_AGENT_NAME
description = (
"Validates the customer-integration-engineer release end to end while "
"preserving source unless a critical defect remains."
)
version = "0.1.0" version = "0.1.0"
config_model = CustomerIntegrationEngineerConfig config_model = CustomerIntegrationEngineerConfig
auth_model = {{ auth_type }} auth_model = NoAuth
# Hosted generated agents read the caller's saved LLM credential through
# ctx.llm. The platform may proxy that credential through LiteLLM, but agent
# code never reads provider keys, LiteLLM master keys, or OPENAI_API_KEY
# directly.
llm_provisioning = LLMProvisioning.PLATFORM
pricing = Pricing( pricing = Pricing(
price_per_call_usd=0.0, price_per_call_usd=0.0,
caller_pays_llm=True, caller_pays_llm=False,
notes="Starter agent uses the caller's saved LLM credential via ctx.llm.", notes="Deterministic validation checks only; no LLM credential required.",
) )
resources = Resources(cpu="250m", memory="512Mi", max_runtime_seconds=300)
workspace_access = WorkspaceAccess.dynamic( workspace_access = WorkspaceAccess.dynamic(
max_files=64, max_files=64,
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
require_reason=False, require_reason=False,
) )
tools_used = ("deepagents", "langchain") tools_used = ("a2a-pack", "workspace-delegation-review")
@a2a.tool(description="Ask the starter DeepAgent to answer with tool calls when useful") @a2a.tool(
async def ask(self, ctx: RunContext[{{ auth_type }}], prompt: str) -> str: description="Validate the live Agent Card version, privacy, five public skills, and workspace safety controls.",
creds = ctx.llm timeout_seconds=60,
await ctx.emit_progress(f"llm: {creds.model} via {creds.source}") idempotent=True,
if not creds.api_key: cost_class="cheap",
return ( )
"LLM key required. Add an LLM credential in Settings > LLM " async def validate_live_card(
"credentials before running this agent; for local --invoke "
"runs set AGENT_LLM_KEY."
)
graph = self._build_deep_agent(ctx=ctx, creds=creds)
state = await graph.ainvoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"recursion_limit": DEEPAGENTS_RECURSION_LIMIT},
)
await ctx.emit_progress("deepagent finished")
return _last_message_text(state)
def _build_deep_agent(
self, self,
*, ctx: RunContext[NoAuth],
ctx: RunContext[{{ auth_type }}], payload: CardValidationInput,
creds: LLMCreds, ) -> dict[str, Any]:
) -> Any: await ctx.emit_progress("validating live card contract")
# Lazy imports keep `a2a card` usable before local dependencies are card = payload.card
# installed. `a2a deploy` installs requirements.txt during the build. skills = _skills(card)
from a2a_pack.deepagents import create_a2a_deep_agent workspace = _workspace_access(card)
from langchain.agents.middleware import wrap_model_call runtime = _runtime(card)
from langchain_core.tools import tool checks = [
_check("agent_name", card.get("name") == self.config.target_agent_name, f"expected {self.config.target_agent_name}"),
_check("version", str(card.get("version")) == payload.expected_version, f"expected live version {payload.expected_version}"),
_check("skill_count", len(skills) == payload.expected_skill_count, f"expected {payload.expected_skill_count} skills, got {len(skills)}"),
_check("private_exposure", _is_private(card) if payload.require_private else True, "agent must remain private"),
_check("workspace_enabled", bool(workspace.get("enabled")), "WorkspaceAccess.dynamic must be enabled"),
_check("workspace_modes", EXPECTED_WORKSPACE_MODES <= set(_mode_values(workspace.get("allowed_modes"))), "requires read_only and read_write_overlay"),
_check("no_llm_required", not _runtime_requires_caller_llm(runtime), "validator/smoke path must not require caller LLM"),
]
return _result("validate_live_card", checks, extra={"skills": [_skill_name(s) for s in skills]})
@tool @a2a.tool(
def text_stats(text: str) -> str: description="Confirm the primary public skill has bounded typed inputs and no credential-shaped parameters.",
"""Return exact word, character, and line counts for text.""" timeout_seconds=60,
words = [part for part in text.split() if part.strip()] idempotent=True,
return json.dumps( cost_class="cheap",
{ )
"characters": len(text), async def validate_primary_skill_schema(
"words": len(words), self,
"lines": len(text.splitlines()) or 1, ctx: RunContext[NoAuth],
} payload: CardValidationInput,
) primary_skill_name: str = "",
) -> dict[str, Any]:
@wrap_model_call await ctx.emit_progress("validating primary skill schema")
async def log_model_call(request: Any, handler: Any) -> Any: skills = _skills(payload.card)
messages = request.state.get("messages", []) primary = _select_skill(skills, primary_skill_name)
print( schema = _input_schema(primary) if primary else {}
"[middleware] model_call " properties = schema.get("properties") if isinstance(schema, dict) else {}
f"model={creds.model} source={creds.source} messages={len(messages)}" required = schema.get("required") if isinstance(schema, dict) else []
) property_names = sorted(properties) if isinstance(properties, dict) else []
return await handler(request) checks = [
_check("primary_skill_present", primary is not None, "primary skill must exist"),
backend = ctx.workspace_backend() _check("schema_is_object", isinstance(schema, dict) and schema.get("type") == "object", "input_schema must be a JSON object"),
skill_sources = _seed_runtime_skills(backend, ctx) _check("bounded_property_count", isinstance(properties, dict) and 1 <= len(properties) <= 12, "primary skill should expose 1-12 typed inputs"),
# create_a2a_deep_agent resolves provider:model strings with _check("additional_properties_false", schema.get("additionalProperties") is False, "schema should reject unknown inputs"),
# langchain.init_chat_model from ctx.llm, preserving LiteLLM routing, _check("all_inputs_typed", _all_properties_typed(properties), "each input property must declare a type or structured schema"),
# provider-specific extra body, and runtime model overrides. _check("no_credential_inputs", not any(SECRET_KEY_RE.search(name) for name in property_names), "public inputs must not request secrets"),
return create_a2a_deep_agent( _check("required_is_bounded", isinstance(required, list) and len(required) <= len(property_names), "required list must be bounded"),
ctx, ]
creds=creds, return _result(
backend=backend, "validate_primary_skill_schema",
skills=skill_sources or None, checks,
tools=[text_stats], extra={"primary_skill": _skill_name(primary) if primary else None, "input_names": property_names},
middleware=[log_model_call],
system_prompt=SYSTEM_PROMPT,
) )
@a2a.tool(
description="Review source-change policy for critical-only edits, no code-editor-agent use, and version bump discipline.",
timeout_seconds=60,
idempotent=True,
cost_class="cheap",
)
async def review_source_change_policy(
self,
ctx: RunContext[NoAuth],
mutated_source: bool,
critical_defect_remaining: bool,
used_code_editor_agent: bool,
improvement_iterations: int = 0,
previous_version: str = EXPECTED_LIVE_VERSION,
resulting_version: str = EXPECTED_LIVE_VERSION,
changed_files: list[str] | None = None,
) -> dict[str, Any]:
await ctx.emit_progress("reviewing source mutation policy")
files = changed_files or []
checks = [
_check("no_code_editor_agent", not used_code_editor_agent, "code-editor-agent must not be used"),
_check("iteration_limit", 0 <= improvement_iterations <= MAX_ALLOWED_IMPROVEMENT_ITERATIONS, "at most 0 improvement iterations allowed"),
_check("critical_only_mutation", (not mutated_source) or critical_defect_remaining, "source may change only when a critical defect remains"),
_check("version_bumped_on_mutation", (not mutated_source) or previous_version != resulting_version, "version must change after real code-editor mutation"),
_check("no_cosmetic_files", not _looks_cosmetic_only(files), "avoid cosmetic-only source changes"),
]
return _result("review_source_change_policy", checks, extra={"changed_files": files})
def _runtime_skills_root(ctx: RunContext[Any]) -> str: @a2a.tool(
workspace = getattr(ctx, "_workspace", None) description="Validate a live smoke-call result from Agent Studio workspace delegation and redact any secret-like values.",
prefixes = tuple(getattr(workspace, "write_prefixes", ()) or ()) timeout_seconds=60,
if not prefixes: idempotent=True,
outputs_prefix = getattr(workspace, "outputs_prefix", None) cost_class="cheap",
prefixes = (outputs_prefix or "outputs/",) )
prefix = str(prefixes[0]).strip("/") async def validate_smoke_call(
return f"/{prefix}/{RUNTIME_SKILLS_DIR}" if prefix else f"/{RUNTIME_SKILLS_DIR}" self,
ctx: RunContext[NoAuth],
smoke: SmokeCallInput,
) -> dict[str, Any]:
await ctx.emit_progress("validating smoke-call result")
secret_paths = _secret_paths(smoke.result)
checks = [
_check("http_success", 200 <= smoke.status_code < 300, f"status_code={smoke.status_code}"),
_check("structured_result", isinstance(smoke.result, dict) and bool(smoke.result), "smoke result must be a non-empty JSON object"),
_check("workspace_delegated", smoke.used_workspace_delegation, "smoke must use Agent Studio workspace delegation"),
_check("delegation_mode_safe", smoke.delegated_workspace_mode in EXPECTED_WORKSPACE_MODES, "delegated mode must be read_only or read_write_overlay"),
_check("no_secrets_returned", not secret_paths, "result must not contain secret-shaped keys or values"),
]
return _result(
"validate_smoke_call",
checks,
extra={"skill_name": smoke.skill_name, "secret_like_paths": secret_paths},
)
@a2a.tool(
description="Combine validation outputs and reviewer findings into a final go/no-go release report.",
timeout_seconds=60,
idempotent=True,
cost_class="cheap",
)
async def final_release_report(
self,
ctx: RunContext[NoAuth],
validation_results: list[dict[str, Any]],
reviewer_findings: list[ReviewerFinding] | None = None,
warnings_to_accept: list[str] | None = None,
) -> dict[str, Any]:
await ctx.emit_progress("building final release report")
findings = reviewer_findings or []
warnings = warnings_to_accept or []
criticals = [f.model_dump() for f in findings if f.severity.lower() == "critical"]
failed_checks = [
{"result_index": i, "check": check}
for i, result in enumerate(validation_results)
for check in result.get("checks", [])
if isinstance(check, dict) and not check.get("ok")
]
residual_warnings = [f.model_dump() for f in findings if f.severity.lower() == "warning"]
all_warnings_accounted = len(residual_warnings) == 0 or bool(warnings)
checks = [
_check("zero_critical_findings", not criticals, "reviewer must report zero critical findings"),
_check("all_validation_checks_pass", not failed_checks, "all prior validation checks must pass"),
_check("warnings_patched_or_listed", all_warnings_accounted, "warnings must be patched or listed as residual risks"),
]
status = "pass" if all(c["ok"] for c in checks) else "fail"
return {
"ok": status == "pass",
"status": status,
"target_agent": self.config.target_agent_name,
"target_version": self.config.target_version,
"checks": checks,
"critical_findings": criticals,
"failed_checks": failed_checks,
"residual_risks": warnings or [f["message"] for f in residual_warnings],
"secrets_included": False,
}
def _seed_runtime_skills(backend: Any, ctx: RunContext[Any]) -> list[str]: def _runtime(card: dict[str, Any]) -> dict[str, Any]:
"""Copy packaged DeepAgents skills into the invocation workspace. value = card.get("runtime")
return value if isinstance(value, dict) else {}
DeepAgents loads skills from its backend, while source-controlled
``skills/`` folders live in the image. This bridge lets generated agents
ship reusable SKILL.md bundles without giving up durable A2A workspace
files.
"""
root = Path(__file__).parent / "skills"
if not root.exists():
return []
runtime_skills_root = _runtime_skills_root(ctx)
uploads: list[tuple[str, bytes]] = []
for path in root.rglob("*"):
if path.is_file():
rel = path.relative_to(root).as_posix()
uploads.append((runtime_skills_root + rel, path.read_bytes()))
if uploads:
backend.upload_files(uploads)
return [runtime_skills_root]
return []
def _last_message_text(state: dict[str, Any]) -> str: def _workspace_access(card: dict[str, Any]) -> dict[str, Any]:
messages = state.get("messages") or [] for key in ("workspace_access", "workspaceAccess", "workspace"):
if not messages: value = card.get(key)
return json.dumps(state, default=str) if isinstance(value, dict):
return value
runtime = _runtime(card)
for key in ("workspace_access", "workspaceAccess", "workspace"):
value = runtime.get(key)
if isinstance(value, dict):
return value
return {}
content = getattr(messages[-1], "content", None)
if isinstance(content, str): def _skills(card: dict[str, Any]) -> list[dict[str, Any]]:
return content value = card.get("skills") or card.get("tools") or []
if isinstance(content, list): return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
parts: list[str] = []
for item in content:
if isinstance(item, dict): def _skill_name(skill: dict[str, Any] | None) -> str:
text = item.get("text") or item.get("content") if not isinstance(skill, dict):
if text: return ""
parts.append(str(text)) return str(skill.get("name") or skill.get("id") or "")
elif item:
parts.append(str(item))
return "\n".join(parts) if parts else json.dumps(content, default=str) def _select_skill(skills: list[dict[str, Any]], name: str) -> dict[str, Any] | None:
return str(content or messages[-1]) if name:
return next((skill for skill in skills if _skill_name(skill) == name), None)
return skills[0] if skills else None
def _input_schema(skill: dict[str, Any] | None) -> dict[str, Any]:
if not isinstance(skill, dict):
return {}
for key in ("input_schema", "inputSchema", "parameters"):
value = skill.get(key)
if isinstance(value, dict):
return value
return {}
def _mode_values(value: Any) -> list[str]:
if isinstance(value, list):
raw = value
elif isinstance(value, tuple):
raw = list(value)
elif isinstance(value, str):
raw = [value]
else:
raw = []
return [str(item.get("value") if isinstance(item, dict) else item).lower() for item in raw]
def _is_private(card: dict[str, Any]) -> bool:
expose = card.get("expose")
if isinstance(expose, dict) and "public" in expose:
return expose.get("public") is False
visibility = str(card.get("visibility") or card.get("access") or "").lower()
if visibility in {"private", "unlisted"}:
return True
public = card.get("public")
return public is False if isinstance(public, bool) else True
def _runtime_requires_caller_llm(runtime: dict[str, Any]) -> bool:
provisioning = str(runtime.get("llm_provisioning") or runtime.get("llmProvisioning") or "").lower()
pricing = runtime.get("pricing") if isinstance(runtime.get("pricing"), dict) else {}
return provisioning in {"platform", "caller_provided", "platform_or_caller_provided"} and bool(pricing.get("caller_pays_llm"))
def _all_properties_typed(properties: Any) -> bool:
if not isinstance(properties, dict) or not properties:
return False
for schema in properties.values():
if not isinstance(schema, dict):
return False
if not any(key in schema for key in ("type", "anyOf", "oneOf", "allOf", "$ref")):
return False
return True
def _looks_cosmetic_only(paths: list[str]) -> bool:
if not paths:
return False
non_cosmetic_exts = {".py", ".yaml", ".yml", ".toml", ".json", ".txt"}
cosmetic_markers = {"readme", "docs/", ".md"}
lowered = [p.lower() for p in paths]
return all(any(marker in p for marker in cosmetic_markers) for p in lowered) and not any(
any(p.endswith(ext) for ext in non_cosmetic_exts - {".txt"}) for p in lowered
)
def _secret_paths(value: Any, prefix: str = "$") -> list[str]:
found: list[str] = []
if isinstance(value, dict):
for key, item in value.items():
path = f"{prefix}.{key}"
if SECRET_KEY_RE.search(str(key)):
found.append(path)
found.extend(_secret_paths(item, path))
elif isinstance(value, list):
for i, item in enumerate(value[:100]):
found.extend(_secret_paths(item, f"{prefix}[{i}]"))
elif isinstance(value, str) and SECRET_VALUE_RE.search(value):
found.append(prefix)
return sorted(set(found))
def _check(name: str, ok: bool, detail: str) -> dict[str, Any]:
return {"name": name, "ok": bool(ok), "detail": detail}
def _result(name: str, checks: list[dict[str, Any]], *, extra: dict[str, Any] | None = None) -> dict[str, Any]:
ok = all(item["ok"] for item in checks)
payload = {"ok": ok, "status": "pass" if ok else "fail", "checkset": name, "checks": checks, "secrets_included": False}
if extra:
payload.update(extra)
return json.loads(json.dumps(payload, default=str))
def _is_safe_url(url: str) -> bool:
parsed = urlparse(url)
return parsed.scheme in {"https", "http"} and bool(parsed.netloc)