Skip to main content

AGENTICSTAR Platform SDK

Enterprise AI Agent Infrastructure SDK for building autonomous agent systems.

Installation

Requires Python 3.11+.

# Core (minimal) — Events / Storage paths / Metering / Auth / Security / Audit / Policy / Workflow
pip install agenticstar-platform

# With specific modules
pip install agenticstar-platform[db]           # PostgreSQL
pip install agenticstar-platform[rag]          # Qdrant + Embedding
pip install agenticstar-platform[storage]      # Azure Blob, S3, GCS
pip install agenticstar-platform[storage-azure] # Azure Blob only (AzureBlobStorageClient)
pip install agenticstar-platform[storage-aws]   # AWS S3 only (S3StorageClient)
pip install agenticstar-platform[storage-gcp]   # GCS only (GCSStorageClient)
pip install agenticstar-platform[memory]       # Semantic memory (Mem0)
pip install agenticstar-platform[security]     # PII detection
pip install agenticstar-platform[security-aws] # AWS content safety (AWSSecurityClient)
pip install agenticstar-platform[webhook]      # aiohttp (WebhookEventHandler)
pip install agenticstar-platform[runner]       # Marketplace runner (run_marketplace_agent)
pip install agenticstar-platform[lab]          # Local Integration Lab
pip install agenticstar-platform[all]          # All modules

Accessing a component through the top-level package (from agenticstar_platform import ...) when its extra is not installed raises an ImportError that names the extra you need, instead of a raw ModuleNotFoundError. (Importing a submodule such as agenticstar_platform.db directly without its extra raises the underlying ModuleNotFoundError.)

ImportError: 'PostgreSQLManager' requires the 'db' extra of agenticstar-platform.
Install it with:  pip install 'agenticstar-platform[db]'  (missing dependency: No module named 'asyncpg')

Storage provider clients follow the same contract. Core types such as StorageConfig / StoragePaths import without any extra, while AzureBlobStorageClient (storage-azure) / S3StorageClient (storage-aws) / GCSStorageClient (storage-gcp) point to the matching extra as soon as the symbol is accessed (not deferred to the constructor). The success / failure boundary is the same whether you use from agenticstar_platform import S3StorageClient or from agenticstar_platform.storage import S3StorageClient.

Quick Start

1. A minimal agent (no external services, copy and run)

Your agent's logic, LLM and framework are entirely your choice — the SDK does not provide them. The SDK takes care of the infrastructure, such as delivering progress and results to the frontend. First, let's run a single path from progress event → terminal event with no external services.

hello_agent.py:

import asyncio

from agenticstar_platform import EventEmitter, EventType, create_json_handler


async def my_agent(emitter: EventEmitter, request: str) -> None:
    """Your agent. The logic is up to you (LangChain / OpenAI Agents SDK / your own)."""
    await emitter.emit_event(EventType.PHASE_START, f"received: {request}")
    try:
        answer = request.upper()  # replace this with your actual logic
        await emitter.emit_event(EventType.COMPLETION_SUCCESS, answer)
    except Exception as e:
        # Always send exactly one terminal event (otherwise the frontend never shows completion)
        await emitter.emit_event(EventType.COMPLETION_FAILURE, str(e))


async def main() -> None:
    emitter = EventEmitter(execution_id="demo-001", handler=create_json_handler())
    asyncio.create_task(my_agent(emitter, "hello agenticstar"))

    # Stops automatically once COMPLETION_SUCCESS / COMPLETION_FAILURE is received
    async for chunk in emitter.consume_events():
        print(chunk)  # create_json_handler output has no newline, so print one line per chunk


if __name__ == "__main__":
    asyncio.run(main())
pip install agenticstar-platform
python hello_agent.py

Output (create_json_handler() returns JSON lines, create_sse_handler() returns SSE format):

{"event_type": "phase_start", "execution_id": "demo-001", "message": "received: hello agenticstar", "timestamp": 1785162261.27}
{"event_type": "completion_success", "execution_id": "demo-001", "message": "HELLO AGENTICSTAR", "timestamp": 1785162261.27}

Change the answer = ... line to raise RuntimeError("upstream timeout") and completion_failure is emitted as the terminal event instead.

Note that emit_event() only enqueues. In setups that do not run consume_events() as above (i.e. without SSE), handlers do not fire until you call EventEmitter.drain().

2. Run the same function Marketplace-compatible (runner)

An agent function that works locally can be passed to run_marketplace_agent as-is to run it with the Marketplace-compatible terminal lifecycle. Receiving and validating the identity (EXECUTION_ID etc.), fetching the input message, persisting the result to the DB, webhook notification, the terminal event (exactly once, whatever happens — the one exception is a cycle execution whose started fence below is refused or cannot be taken, which sends no events) and cleanup are all handled by the runner — none of it is written in your agent. For a cycle execution (execution_data.runtime_protocol = mp-chat/1) the runner also takes a started fence in the DB before calling your agent, so a recreated Pod does not run the same execution again (3.0.12). If you replay a cycle execution locally against a real DB with the same CONVERSATION_ID / MESSAGE_ID, the second run is refused with MarketplaceRunnerFenceTaken.

marketplace_agent.py:

from agenticstar_platform import run_marketplace_agent


async def my_agent(emitter, message: str) -> str:
    """Your agent. The logic is up to you (LangChain / OpenAI Agents SDK / your own)."""
    return message.upper()  # replace this with your actual logic


if __name__ == "__main__":
    run_marketplace_agent(my_agent)
pip install 'agenticstar-platform[runner]'
python marketplace_agent.py

Environment variable contract:

Variable Set by Required
EXECUTION_ID / CONVERSATION_ID / USER_ID / MESSAGE_ID Injected by the Marketplace executor when the pod starts ✅
REQUEST_SOURCE / AGENT_ID Same as above (optional) —
DB_HOST / DB_PORT / DB_DATABASE / DB_USER / DB_PASSWORD Env var settings at agent registration (PostgreSQLConfig.from_env() contract) ✅
WEBHOOK_URL Env var settings at agent registration ✅

Terminal event rules:

  • The agent function's return value is saved and delivered as the body of completion_success
  • If the agent function raises, the run converges to completion_failure (the traceback goes to logs only, never to the event body)
  • Agents that emit their own terminal event, like the local sample above, do not cause a duplicate (the same function works in both setups)
  • If a required env var is missing, the runner stops with MarketplaceRunnerConfigError without calling the agent

If you need the low-level API (to compose handlers yourself), pass create_marketplace_handler(data_access, webhook_url, user_id, conversation_id, message_id) (a composite handler for DB persistence + webhook notification; requires the [db] + [webhook] extras) directly to EventEmitter, as before.

2.4 Report completion for a cycle execution (SDK >= 3.0.14, mp-chat/1)

A cycle execution (execution_data.runtime_protocol = mp-chat/1) that just exits 0 is recorded as "the run finished, whether it achieved its goal is not measured" (outcome NULL, outcome_reason_code = runner_exited), so it is counted neither as a success nor as a failure. To have it counted, your agent reports whether it completed its goal — the platform never infers it from the process exiting cleanly. report_completion needs the [db] extra; the example below also uses the runner, so install [runner] (or [all]), which includes it.

The runner does not hand its connection pool to the agent function, so build the DataAccess yourself, pass it to the runner and report through its pool:

import asyncio
import os

from agenticstar_platform import arun_marketplace_agent, report_completion
from agenticstar_platform.db import DataAccess, PostgreSQLConfig, PostgreSQLManager


async def main() -> None:
    manager = PostgreSQLManager(PostgreSQLConfig.from_env())
    async with DataAccess(manager) as data_access:

        async def my_agent(emitter, message: str) -> str:
            reply = message.upper()  # your processing
            await report_completion(
                manager.pool,                       # asyncpg pool (PostgreSQLManager.pool)
                user_id=os.environ["USER_ID"],
                conversation_id=os.environ["CONVERSATION_ID"],
                message_id=os.environ["MESSAGE_ID"],
                execution_id=os.environ["EXECUTION_ID"],
                outcome="completed",                # one of the 7 saved values
                reason="completed",                 # short snake_case code; defaults to the outcome
                had_mutating_success=False,         # wrote something outside? None if unknown
                summary="posted the daily digest",  # optional, for people reading the record
            )
            return reply

        await arun_marketplace_agent(my_agent, data_access=data_access)


if __name__ == "__main__":
    asyncio.run(main())
  • outcome is one of completed / incomplete / failed / timeout / stopped / denied / skipped. Anything else raises ValueError
  • reason is a reason code, not a message: lowercase snake_case of at most 32 characters (^[a-z][a-z0-9_]{0,31}$; - and spaces become _, upper case becomes lower case). Any other value (a sentence, an exception string) is replaced by the outcome and a warning is logged. Put free text in summary
  • The platform uses only outcome and reason for the execution's result. had_mutating_success and summary are kept in the report row
  • Reporting is optional. Without a report the execution stays unmeasured, exactly as before
  • Report once, when the result is final. A second report for the same execution — also one made at the same time — is not written and returns False (the first one is kept)
  • Reporting is best effort and never fails your agent. A database failure (including waiting more than 30 seconds for a connection) is logged and returns False; then the execution stays unmeasured. False after a lost commit acknowledgement can still mean the report was saved
  • The executor adopts the report only when it sees the container exit 0. run_marketplace_agent exits 0 even when your agent raises, so a report made before the exception is adopted. When the executor closes the execution otherwise — a non-zero exit or a lost Pod (failed, effects unknown), a cancellation (stopped/cancelled) or the deadline passing while the agent is still running (timeout/execution_timeout) — the report is not used
  • Reporting goes through the DB because a cycle agent has no runtime API (the same reason the started fence uses the DB). No DDL is involved: the report is one row in execution_messages. In a normal (chat) execution the report is written but not read

2.5 Local Integration Lab (DB / RAG / storage without credentials)

As the next step after Hello World, the package bundles a lab that starts PostgreSQL, Qdrant, S3-compatible storage and a deterministic offline embedding with a version-pinned Docker Compose file, and runs a synthetic document through ingest → retrieve → artifact/result persist → terminal outcome in a single command. No cloud account, production credentials or .env editing is needed (just Docker and Python 3.11+).

The lab ships inside the package, so installing from PyPI is all you need:

pip install 'agenticstar-platform[lab]'
python -m agenticstar_platform.lab     # diagnostics: same command + doctor; teardown: --reset

Local copies of artifacts are placed in agenticstar-lab-artifacts/ under the current working directory. For details, see agenticstar_platform/lab/README.md in the installed package.

3. Initializing the infrastructure components

Below are initialization examples for each component (excerpt; running them requires connection details and credentials for the corresponding external services, and the matching extras).

from agenticstar_platform import (
    # Database
    PostgreSQLManager, ApiPostgreSQLManager, PostgreSQLConfig, DataAccess,
    # RAG (Vector DB + Embedding)
    QdrantManager, QdrantConfig, EmbeddingGenerator, EmbeddingConfig,
    # Storage
    AzureBlobStorageClient, AzureBlobConfig,
    # Events
    EventEmitter, EventType,
    # Auth
    AgenticStarAuthClient, AgenticStarAuthConfig,
    # Memory
    SemanticMemoryClient, SemanticMemoryConfig,
)

# Example: Initialize SDK components
async def main():
    # Database (direct connection)
    db_config = PostgreSQLConfig.from_toml("config.toml", section="database")
    manager = PostgreSQLManager(db_config)
    da = DataAccess(manager)
    await da.initialize()
    users = await da.fetch_all("SELECT * FROM users WHERE active = $1", (True,))

    # Database (HTTP API)
    db_config = PostgreSQLConfig(api_url="https://your-api.example.com/db")
    manager = ApiPostgreSQLManager(db_config, token_provider=lambda: "your-token")
    da = DataAccess(manager)
    users = await da.fetch_all("SELECT * FROM users WHERE active = $1", (True,))

    # RAG System
    embedding_config = EmbeddingConfig.from_toml("config.toml", section="rag.embedding")
    embedding_gen = EmbeddingGenerator(embedding_config)

    qdrant_config = QdrantConfig.from_toml("config.toml", section="rag.qdrant")
    async with QdrantManager(qdrant_config, embedding_gen) as qdrant:
        results = await qdrant.search("How to use the SDK?", limit=5)

    # Storage (uses from_dict, not from_toml)
    storage_config = AzureBlobConfig.from_dict({
        "bucket_name": "your-container",
        "connection_string": "your-connection-string",
    })
    storage = AzureBlobStorageClient(storage_config)

Modules

Module Extra Description
db [db] PostgreSQL data access layer with Azure AD support
rag [rag] Qdrant vector database and Azure OpenAI / OpenAI-compatible embedding integration
storage [storage] / [storage-azure] / [storage-aws] / [storage-gcp] Multi-cloud storage (Azure Blob, S3, GCS)
auth (core) AgenticStar Auth API client (authentication, user management, MCP tokens)
memory [memory] Semantic memory (Mem0 + Qdrant)
security [security] PII detection (Azure Presidio, AWS Bedrock Guardrails / Comprehend, GCP DLP)
events (core) Event type definitions for streaming
common (core) Shared utilities (secret masking, validation)
metering (core) LLM usage & cost ledger (UsageMeter)
runner [runner] Marketplace-compatible run lifecycle (run_marketplace_agent)
audit (core) Append-only action-audit ledgers and guardrail alert writers
policy (core) Tool-call governance policy (pure computation)
workflow (core) Runner for agents in project workflow steps (mp-workflow/1; Marketplace edition 3.1+)

Auth Module

from agenticstar_platform.auth import AgenticStarAuthClient, AgenticStarAuthConfig

# From config.toml [auth.agenticstar] section
config = AgenticStarAuthConfig.from_config("config.toml")
client = AgenticStarAuthClient(config)

# Get user info
user = await client.get_user(user_id="user-001")

# Get MCP tokens
tokens = await client.get_mcp_tokens(user_id="user-001")

# After an MCP server rejected a token with 401 before its stored expiry, ask again with the
# SHA-256 of the rejected token (SDK >= 3.0.10). The token supply refreshes it even before expiry.
import hashlib
rejected_access_token = "..."  # the access token the MCP server answered 401 to
tokens = await client.get_mcp_tokens(
    user_id="user-001",
    providers=["slack_mcp"],
    rejected_token_sha256={"slack_mcp": hashlib.sha256(rejected_access_token.encode("utf-8")).hexdigest()},
)

Memory Module

Semantic memory powered by Mem0 + Qdrant (requires pip install agenticstar-platform[memory]):

from agenticstar_platform.memory import SemanticMemoryClient, SemanticMemoryConfig

config = SemanticMemoryConfig.from_toml("config.toml")
memory = SemanticMemoryClient(config)

# Add memory (methods are synchronous; only cleanup() is async)
memory.add(
    [{"role": "user", "content": "User prefers dark mode"}],
    user_id="user-001",
)

# Search memory
results = memory.search("user preferences", user_id="user-001")

Storage Module

Note: AzureBlobConfig uses from_dict() (not from_toml()):

from agenticstar_platform.storage import AzureBlobStorageClient, AzureBlobConfig

config = AzureBlobConfig.from_dict({
    "bucket_name": "your-container",
    "connection_string": "DefaultEndpointsProtocol=https;...",
    "prefix": "uploads/",
})
client = AzureBlobStorageClient(config)
result = await client.upload_file("local/file.pdf", prefix="docs/")

Azure without a connection string (3.1.0): leave connection_string out (or empty) and give storage_account_name. The client then signs in with DefaultAzureCredential (Workload Identity or Managed Identity on AKS, Azure CLI on a workstation), which is what you need once the storage account has Shared Key access disabled. The identity needs Storage Blob Data Contributor on the account (or container). endpoint_suffix defaults to core.windows.net.

config = AzureBlobConfig.from_dict({
    "bucket_name": "your-container",
    "storage_account_name": "yourstorageaccount",
})
client = AzureBlobStorageClient(config)

Telemetry / LLM Usage Tracking (Marketplace)

TelemetryAccess (under db module) writes records to the ai_telemetry table. Unknown fields are stored in the metadata jsonb column automatically — no schema migration is required to add new tracking dimensions.

For Marketplace agents that wrap LLM calls, the SDK defines recommended field names for token and model usage. Following this convention enables cross-agent cost / utilization analytics in shared dashboards.

from agenticstar_platform.db import TelemetryAccess

telemetry = TelemetryAccess(data_access)

# After an LLM call from your custom agent:
response = await openai_client.chat.completions.create(...)

await telemetry.save_telemetry({
    "conversation_id": conversation_id,
    "agent_type": "my_marketplace_agent",
    "service": "my-agent-service",
    "operation": "generate_response",
    "duration_ms": elapsed_ms,
    "success": True,

    # Recommended convention fields (stored automatically in metadata jsonb)
    "prompt_tokens": response.usage.prompt_tokens,
    "completion_tokens": response.usage.completion_tokens,
    "total_tokens": response.usage.total_tokens,
    "model": "azure/gpt-4.1",  # LiteLLM-style identifier
})
Field Type Source Notes
prompt_tokens int usage.prompt_tokens (OpenAI / LiteLLM compatible) Input tokens
completion_tokens int usage.completion_tokens Output tokens
total_tokens int usage.total_tokens Sum
model str LiteLLM-style: azure/gpt-4.1, bedrock/anthropic.claude-3-5-sonnet, openai/gpt-4o, etc. Provider/model identifier

These fields are not known columns — they land in metadata jsonb automatically. No SDK code change, no DB schema migration. Use the recommended names so that your data joins with platform-level analytics.

Known columns (reserved field names)

The following keys are written to their own ai_telemetry columns instead of metadata. Any other key lands in metadata jsonb.

timestamp, service, operation, duration_ms, conversation_id, intent_type, confidence_score, tools_used, success, agent_type, agent_level, task_complexity, current_message, context_summary, suggested_approach, conversation_goal, final_content, metadata

tools_used is an integer column holding a count:

  • int is stored as-is; a list / tuple is normalized to its length.
  • Not measured is NULL; measured-and-zero is 0 — the two are kept distinct.
  • Values the column cannot hold (a breakdown list, a wrong type, or a value outside the PostgreSQL integer range) are additionally kept under metadata.tools_used, so nothing is lost. The column value itself never fails a telemetry write; a raw value kept in metadata follows the same JSON-serializability rule as any other metadata field.

tools_used became a known column in 0.5.38 (use 0.5.39 or later). Before that it fell through to metadata; if you read metadata->>'tools_used' for plain integer counts, read the column instead.

Cross-agent analytics example

-- Per-model token usage in the last 30 days
SELECT
  metadata->>'model' AS model,
  agent_type,
  SUM((metadata->>'prompt_tokens')::int)     AS total_prompt_tokens,
  SUM((metadata->>'completion_tokens')::int) AS total_completion_tokens,
  COUNT(*)                                    AS invocations,
  AVG(duration_ms)::int                       AS avg_duration_ms
FROM ai_telemetry
WHERE timestamp > NOW() - INTERVAL '30 days'
  AND metadata ? 'prompt_tokens'
GROUP BY model, agent_type
ORDER BY total_prompt_tokens DESC;

Updated in SDK ≥ 0.5.15: Cost conversion is no longer out of scope. The new Metering module (UsageMeter) below computes cost via a pluggable engine (litellm by default — its community-maintained price map solves the "changes too frequently" problem; graceful NULL when litellm is absent). TelemetryAccess remains the place for agent operational telemetry (intent / tools / duration → ai_telemetry); UsageMeter is the dedicated per-LLM-call cost ledger. Use whichever fits; they are complementary.

Metering Module — UsageMeter (SDK ≥ 0.5.15)

Dedicated LLM usage & cost infrastructure: computes cost from an LLM response/usage and records one row per call to llm_usage_ledger, with a daily rollup (llm_usage_daily) and cost-visualization queries. Pure infra — agent-logic agnostic, identical for self-hosted and Marketplace BYO (in-process; no proxy/header coupling).

  • Pluggable cost: default uses litellm (completion_cost / cost_per_token) — reflects long-context, prompt-cache and tier pricing. If litellm is absent, tokens are still recorded (cost_usd = NULL). Pass cost_fn=... to override.
  • DB: any handle exposing execute_query(query, params) -> {success, data, error} (the SDK's DataAccess / PostgreSQLManager).
  • Usage status (SDK ≥ 0.5.21): each row records usage_status (present/missing); when usage is absent, pass missing_reason= (e.g. provider_omitted / stream_interrupted). It is stored on the row (not just logs) so offline exports can tell cost_usd IS NULL (unpriced) vs usage_status='missing' vs true-zero apart. Present rows always store NULL (no contradiction).
  • Prompt-cache breakdown (SDK ≥ 3.0.0): cache reads and cache writes are recorded separately in cache_read_tokens / cache_creation_tokens (both nullable; cached_tokens is kept as before). See the 3.0.0 changelog entry before upgrading writers on an existing ledger.
from agenticstar_platform.metering import UsageMeter

meter = UsageMeter(db=data_access)
await meter.ensure_schema()                        # create ledger/daily/rollup if absent (idempotent)

# Record one LLM call (cost computed automatically; fire-and-forget safe)
await meter.record(
    model="gpt-5.5", response=resp, endpoint="chat/completions",
    labels={"execution_id": eid, "message_id": mid, "conversation_id": cid, "user_id": uid},
)

# ...or wrap the call so it records on completion
resp = await meter.track(model="gpt-5.5", labels=ids)(litellm.acompletion)(**params)

# Cost only (no record)
usd = UsageMeter.cost_usd("gpt-5.5", response=resp)

# Daily rollup + cost visualization (for dashboards / billing)
await meter.rollup_recent()
rows = await meter.daily_cost(by="model", since_days=30)   # by = "model" | "user" | "agent" | "day"

Schema (ensure_schema): llm_usage_ledger — one row per call (execution / message / conversation / user, model, in/out/total/cached tokens, cache read / creation tokens, usage_status + missing_reason, cost_usd, currency) — plus llm_usage_daily (rollup) and rollup_llm_usage_daily(day). The default DDL is portable (any PostgreSQL); at scale, partition llm_usage_ledger monthly (e.g. pg_partman).

Audit Module — ActionAudit / GatewayActionAudit (SDK ≥ 0.5.31)

Dedicated action-audit ledger infrastructure: records agent-side action events (approvals, tool authorizations, policy violations, kill-switch, external-agent egress) into append-only ledgers (agent_action_audit / llm_gateway_action_audit). Distinct from metering by its write guarantees — this is an audit trail, not telemetry. Pure infra, no extra dependencies (stdlib only, available in the core install).

  • Callers pass no raw content (usage contract): never hand prompts / tool arguments to the ledger — one-way them with canonical_digest() (SHA-256) into payload_digest. The SDK performs no automatic secret detection or redaction. Field caps (reason truncated to 300 chars, payload_digest must be 64 lowercase-hex chars or it is dropped, metadata over 2KB of key-sorted JSON replaced by {truncated, size_bytes, digest}, always deep-copied) bound the blast radius of accidental leakage — they are not leak prevention by themselves. Numbers in metadata other than integers with an absolute value below 2^53 (floats, larger integers, NaN / Inf) are stored as strings so the row hash is identical across languages.
  • Three write modes: record() = at-least-once while the process lives (bounded retry, then the sanitized row is spilled as JSON to a log line tagged action_audit_spill for manual reconcile) / record_sync() = audit-before-act (raises ActionAuditWriteError on DB write failure — an approval that cannot be audited must not take effect) / record_nowait() = fire-and-forget for hot paths (strong task refs, backlog capped at 512 — overflow spills instead of blocking; pre-start cancellation also spills; best-effort — lost if the process dies abruptly).
  • Decision events require actor_ref: approval.*, gate.resolved, content.deleted, forensic.*, policy.changed, item.quarantine_released and workitem.* raise ValueError synchronously without it (SDK ≥ 1.0.0a1; before that only approval.*).

Guardrail Alerts — GuardrailAlertWriter / GuardrailAlertAggregates (SDK ≥ 0.5.32)

Producer writers for the Admin-facing guardrail alert triage view (guardrail_alerts) and the unlinked daily aggregates (guardrail_alert_daily_aggregates). Non-authoritative operational view — the authority remains the action-audit ledger. Same delivery contract as the audit writers (bounded FAF + retry → spill → drain).

  • No end-user identity: the row schema has no user-id column by design; the aggregates writer takes no correlation-id arguments at all (structural unlinking).
  • SelfHarm never appears on rows: normalized detection (case / space / _ / - variants, str-Enum .value) raises ValueError synchronously; SelfHarm-only detections are recorded as aggregates only. Row policy_outcome is always blocked.
  • Idempotent rows: event_key (v2:{surface}:{correlation_kind}:{correlation}:{decision_point}:{outcome})
    • ON CONFLICT (event_key) DO NOTHING — replays never clobber Admin lifecycle state.
  • Grants differ per table: rows = producer INSERT-only; aggregates = INSERT + UPDATE(count) + SELECT(count) (the UPSERT references the existing counter). Severities are measured Azure category severities (2/4/6) — do not substitute thresholds; aggregate counts are approximate (at-least-once may double-count).
  • DB: any handle exposing execute_query(query, params) -> {success, data, error} (the SDK's DataAccess / PostgreSQLManager).
from agenticstar_platform.audit import ActionAudit, GatewayActionAudit, canonical_digest

audit = ActionAudit(db=data_access, source="my-agent", system_version=image_tag)  # source = name of the writing component
await audit.ensure_schema()                       # provisioning (owner role): create ledger + indexes, add newer columns to older tables

# Hot path (tool authorization etc.): fire-and-forget
audit.record_nowait(
    event_type="tool.access", actor_type="agent", decision="deny",
    resource="bash", action="execute", reason="blocked_command",
    conversation_id=cid, execution_id=eid,
    payload_digest=canonical_digest({"command": cmd}),
)

# Approval (audit-before-act): abort the action if this raises
await audit.record_sync(
    event_type="approval.granted", actor_type="human", actor_ref=approver_id,
    decision="granted", resource=tool_name, conversation_id=cid,
)

await audit.aclose()                              # shutdown: drain pending writes (timeout → cancel + spill)

gw = GatewayActionAudit(db=gateway_da)            # LLM Gateway policy decisions (virtual-key identity)
await gw.record(event_type="policy.mcp_stripped", decision="stripped",
                virtual_key_id=vk, request_id=rid)

Schema (ensure_schema): agent_action_audit (source, event_type, actor, decision, reason, policy, correlation ids, payload_digest, metadata jsonb, row_hash) and llm_gateway_action_audit (virtual key, request ids, event/decision, detail_digest, row_hash). ensure_schema applies REVOKE UPDATE/DELETE best-effort; for production append-only enforcement use a dedicated insert-only role.

Event vocabulary: event_type is a free string; the names the platform's views aggregate are listed in KNOWN_EVENT_TYPES (with one-line meanings) — e.g. tool.access / tool.invoked / tool.effect (tool governance), context.manifest / plan.revised / data.access (provenance), hitl.requested / approval.* / gate.resolved (human touchpoints), delivery.*, runtime.*. actor_kind is a closed vocabulary (ACTOR_KINDS); reference_keys(**refs) builds the correlation keys for metadata (project_id / run_id / step_id / item_id / tool_call_id / root_execution_id). The columns added in 1.0.0a1 (actor_kind, project_id, root_execution_id, source_event_id) are optional on record*(); on a database that lacks them the writer falls back to the legacy insert once and keeps going (while it does, the new column values are not stored and the row hash is computed over the stored columns; it switches back after ensure_schema() adds them, or after a restart once they exist). ensure_schema() needs the table owner role: run it once when provisioning; the runtime writer only needs INSERT. Pre-existing duplicate source_event_id values block the UNIQUE index (deduplicate first); once it exists, a duplicate write makes record() return False (spilled) and record_sync() raise.

Workflow Runner — agenticstar_platform.workflow (SDK ≥ 1.0.1, mp-workflow/1)

A runner for Marketplace workflow runs (a single run of an agent for one step of a workflow). It requires a platform that provides the workflow runtime API (mp-workflow/1; the Marketplace edition from 3.1); agents for regular Marketplace chat use run_marketplace_agent instead. It runs only on ASTER_RUNTIME_PROTOCOL / ASTER_RUNTIME_BASE_URL / ASTER_RUNTIME_TOKEN / EXECUTION_ID, which the platform executor injects per execution (no fallback to DB settings; import agenticstar_platform.workflow needs no database driver).

# my_agent/main.py
async def run(context):
    inv = await context.audit.tool_invoked("read_instructions", effect_kind="read", target="instructions.md")
    instructions = (context.work_dir / "instructions.md").read_text(encoding="utf-8")
    await context.audit.tool_effect(inv, status="ok", count=1)
    (context.output_dir / "reply.md").write_text(await create_reply(context.parameters["product_code"], instructions))
# the image's start command
python -c "from agenticstar_platform.workflow import run; run('my_agent.main:run')"

Write the start command in exec form so that the runner is PID 1: CMD ["python", "-c", "from agenticstar_platform.workflow import run; run('my_agent.main:run')"]. The runner keeps ASTER_RUNTIME_TOKEN away from your agent's processes: the child does not inherit it, and from 3.0.13 run() also makes the runner process unreadable through /proc/<pid>/environ (Linux). A shell form (CMD python -c ..., sh -c "...") or an init process in front of the runner keeps another process holding the token, which commands run by your agent can read; the runner logs a warning when PID 1 holds it.

What the runner (parent process) does: validates the env → fetches the context and input artifacts from the platform runtime API (verifying digest / size) → stages them read-only in /workspace/input → copies them to /workspace/work → generates settings files from from_parameters → waits for the context.manifest audit acknowledgement → runs your agent entrypoint in a child process → collects declared outputs and edited files (rejecting symlinks, path traversal and oversized files) → saves the artifacts → saves the completion_envelope (version 2) under a stable completion_id (a 409 for an already-terminal run is not retried). Exit codes: 0 = terminal saved (or the run was cancelled and the platform refused the completion; the platform closes it as stopped), 2 = could not be saved, 3 = invalid configuration. Other failures may end with a non-zero exit and the exception (the SDK does not guarantee exit codes; the platform closes a non-zero exit without a saved terminal as failed/agent_exit_nonzero with the effect unknown, and holds the run for human review).

What context provides: correlation (execution_id / run_id / operation_key / entry_ref / attempt), parameters, input_dir / work_dir / output_dir, audit.tool_invoked / tool_effect / tool_denied (tool_invoked returns only after the event is acknowledged as saved — do not start an external write without that acknowledgement), is_cancel_requested() / ensure_can_act() / deadline_remaining(), skip(reason) (a step with nothing to do: no_target / not_applicable / nothing_to_do → skipped), policy_unavailable() (the policy could not be checked before any external action → skipped/policy_unavailable), log(message) (written to the runner's log; run() sends SDK logs at INFO to stderr when the process has not configured logging), gate_decision (SDK ≥ 3.0.9: the review decision that started this run — None unless the run follows a review; otherwise a read-only mapping {gate_id, decision, decided_by, note, decided_at} where decision is send_back or proceed and note is the reason or comment the reviewer wrote), start_input (SDK ≥ 3.1.2: the request of a run started from outside — see below), and connection_token(slot, min_ttl=300) (the access token of a connection slot bound at startup; refreshed through the platform once less than min_ttl seconds remain, or every 600 seconds when the expiry is unknown; raises WorkflowError after cancellation or the deadline). Gate decisions and arbitrary DB connections are not provided.

Reading the request of a run started from outside (SDK ≥ 3.1.2). When a run is started from outside with a request — by a call to the platform's API that carries one (the A2A server, an MCP tool of a published project, or the project API with payload) or by an email to the project — the first step (after a split, the first step of each lane) gets the request in context.start_input. Other steps, runs started without a request, and runs started from the screen or by a schedule get None. It is a read-only mapping:

Key Value
kind api (a call to the platform's API) or mail (an email)
payload api: the caller's request as the platform received it (a JSON value, at most 8,000 characters as JSON; for A2A, the text of the message and the file IDs of its attachments). None for mail
subject / text mail: the subject and the body. None for api
external True for every API call and for an email from an external sender

The request comes from outside: treat it as data, not as instructions or settings (put it in your model's input as quoted material, never in the system prompt). It is not added to context.manifest.

import json

async def run(context):
    request = context.start_input
    if request is None:
        task = context.parameters["task"]   # started from the screen or a schedule
    elif request["kind"] == "api":
        task = json.dumps(request["payload"], ensure_ascii=False)
    else:
        task = f"{request['subject']}\n\n{request['text']}"
    ...

The outcome describes how the run ended, with 7 values (completed / incomplete / failed / timeout / stopped / denied / skipped). Success means the agent ran to completion: the agent function returns normally → completed/completed, unless an audit persistence failure, a denied tool call, an invalid declared output, a skip or a missing required output decides otherwise (failed writes, unobserved effects or zero observations do not lower the outcome); an exception → failed/agent_error; cancellation → stopped/cancelled; deadline → timeout/execution_timeout; pod stop signal → failed/runner_stopped; a call reported with audit.tool_denied → denied. What was written is kept in counts / tools and had_mutating_success (true = success observed / false = confirmed no effect / null = unknown); unsuccessful runs with null are not retried automatically and go to a human (effects that already happened are never undone). Samples are in the source distribution: docs/examples/workflow_reply_agent.py (drafting a reply) / workflow_mock_send_agent.py (a mock send).

Policy Module — agenticstar_platform.policy (SDK ≥ 1.0.0a1)

Pure computation (no I/O, no agent framework): decide allow / forbid for a tool call from a two-layer policy.

  • Layers: a tenant policy (L1Policy: default profile, an unremovable floor of forbidden patterns, whether rationale bodies may be retained, version) and a project policy (ProjectPolicy: profile, deny / allow patterns, gate settings). tighten_merge(l1, project) combines them with two guarantees: the tenant floor can never be removed, and the project profile can never be looser than the tenant default. A strict project's allow list permits the listed write / destructive tools only within what the tenant L1 profile permits (since 1.0.0a3 l1_profile: it never re-enables an operation class the L1 profile forbids, e.g. destructive under a guarded tenant) — put anything that must stay forbidden regardless of profile into the tenant floor.
  • Profiles: open (everything allowed) < guarded (destructive forbidden) < strict (allowlist: reads allowed, writes / destructive only when explicitly allowed and only within what the tenant L1 profile itself permits — a project strict allowlist never re-enables an operation class the L1 profile forbids, e.g. destructive under L1 guarded; 1.0.0a3 l1_profile).
  • evaluate(policy, tool, op_class, ...) returns a Decision(mode, reason, policy_version, unregistered, matched, layer). For a forbid, layer tells which rule decided it: project (the project's settings — deny rules, profile, allowlist, dry run — can change it), tenant (the tenant's default profile), floor (the tenant's floor, which nobody can relax) or system (not a rule: no policy, or the arguments could not be evaluated). It is None for an allow. The caller classifies the tool (op_class = read / write / destructive, or None for unknown); for built-in tools, dry_run=True forbids everything but reads; mcp_server="x" matches both mcp:x:<tool> and <tool>.
  • MCP and A2A calls (SDK ≥ 3.0.3 / 3.0.4): pass mcp=True with the connection's connection_id, or connection_kind="a2a" with the A2A connection's connection_id. These calls are not classified by operation class and are allowed (reason mcp_unrestricted / a2a_unrestricted) unless the floor or an explicit forbid rule matches — an external rule for that connection ID, or an unscoped tool-name glob such as mcp:<server>:<tool>. An MCP call without connection_id while external MCP rules exist, or an A2A call without connection_id while A2A rules exist, cannot be identified and is forbidden.
  • Snapshots: EffectivePolicy.to_dict() / from_dict() round-trip and digest (SHA-256) let you record which policy a run was evaluated under and verify it later.
from agenticstar_platform.policy import L1Policy, ProjectPolicy, tighten_merge, evaluate

l1 = L1Policy.from_row({"version": 3, "default_profile": "guarded", "floor": {"forbid": ["mcp:*:delete_*"]}})
# allow patterns match built-in tool names, not operation classes (MCP tools are not subject to the allowlist)
project = ProjectPolicy.from_json({"profile": "strict", "allow": ["write_file"]})
policy = tighten_merge(l1, project, project_id="p1")

decision = evaluate(policy, "delete_repo", None, mcp=True, mcp_server="gitlab", connection_id="12")
assert decision.mode == "forbid" and decision.reason == "floor"   # the tenant floor wins, nobody can loosen it
decision = evaluate(policy, "create_issue", None, mcp=True, mcp_server="gitlab", connection_id="12")
assert decision.reason == "mcp_unrestricted"                        # MCP: allowed unless a forbid rule matches
decision = evaluate(policy, "write_file", "write")
assert decision.mode == "allow"                                     # built-in tool explicitly allowed under strict
decision = evaluate(policy, "delete_file", "destructive")
assert decision.mode == "forbid"                                    # not allowed under strict
print(policy.digest)                        # store next to the run for later verification

Platform Class Example

Below is an example of a Platform class that wraps SDK components for your agent system:

"""
Platform class example - Using AGENTICSTAR Platform SDK
"""
import asyncio
from dataclasses import dataclass
from typing import Optional

from agenticstar_platform import (
    PostgreSQLManager, PostgreSQLConfig, DataAccess,
    QdrantManager, QdrantConfig,
    EmbeddingGenerator, EmbeddingConfig,
    EventEmitter, EventType,
    SemanticMemoryClient, SemanticMemoryConfig,
    AzureBlobStorageClient, AzureBlobConfig,
    AgenticStarAuthClient, AgenticStarAuthConfig,
)


@dataclass
class PlatformConfig:
    """Platform configuration"""
    db_config: PostgreSQLConfig
    qdrant_config: QdrantConfig
    embedding_config: EmbeddingConfig
    storage_config: Optional[AzureBlobConfig] = None
    memory_config: Optional[SemanticMemoryConfig] = None
    auth_config: Optional[AgenticStarAuthConfig] = None

    @classmethod
    def from_toml(cls, path: str) -> "PlatformConfig":
        """Load all configurations from TOML file"""
        return cls(
            db_config=PostgreSQLConfig.from_toml(path, section="database"),
            qdrant_config=QdrantConfig.from_toml(path, section="rag.qdrant"),
            embedding_config=EmbeddingConfig.from_toml(path, section="rag.embedding"),
            storage_config=AzureBlobConfig.from_dict({
                # Load from environment or config
                "bucket_name": "your-container",
                "connection_string": "your-connection-string",
            }),
            auth_config=AgenticStarAuthConfig.from_config(path),
        )


class AgentPlatform:
    """
    Platform class wrapping SDK components.

    Example:
        >>> config = PlatformConfig.from_toml("config.toml")
        >>> platform = AgentPlatform(config)
        >>> await platform.initialize()
        >>>
        >>> # Use database
        >>> users = await platform.db.fetch_all("SELECT * FROM users")
        >>>
        >>> # Use RAG
        >>> results = await platform.search_knowledge("How to deploy?")
        >>>
        >>> # Clean up
        >>> await platform.cleanup()
    """

    def __init__(self, config: PlatformConfig):
        self.config = config
        self._db: Optional[DataAccess] = None
        self._qdrant: Optional[QdrantManager] = None
        self._embedding: Optional[EmbeddingGenerator] = None
        self._storage: Optional[AzureBlobStorageClient] = None
        self._memory: Optional[SemanticMemoryClient] = None
        self._auth: Optional[AgenticStarAuthClient] = None

    async def initialize(self) -> None:
        """Initialize all SDK components"""
        # Database
        db_manager = PostgreSQLManager(self.config.db_config)
        self._db = DataAccess(db_manager)
        await self._db.initialize()

        # Embedding generator
        self._embedding = EmbeddingGenerator(self.config.embedding_config)

        # Vector DB (RAG)
        self._qdrant = QdrantManager(self.config.qdrant_config, self._embedding)
        await self._qdrant.initialize()

        # Storage (optional)
        if self.config.storage_config:
            self._storage = AzureBlobStorageClient(self.config.storage_config)

        # Memory (optional)
        if self.config.memory_config:
            self._memory = SemanticMemoryClient(self.config.memory_config)

        # Auth (optional)
        if self.config.auth_config:
            self._auth = AgenticStarAuthClient(self.config.auth_config)

    @property
    def db(self) -> DataAccess:
        if not self._db:
            raise RuntimeError("Platform not initialized. Call initialize() first.")
        return self._db

    @property
    def qdrant(self) -> QdrantManager:
        if not self._qdrant:
            raise RuntimeError("Platform not initialized. Call initialize() first.")
        return self._qdrant

    @property
    def storage(self) -> Optional[AzureBlobStorageClient]:
        return self._storage

    @property
    def memory(self) -> Optional[SemanticMemoryClient]:
        return self._memory

    @property
    def auth(self) -> Optional[AgenticStarAuthClient]:
        return self._auth

    async def search_knowledge(self, query: str, limit: int = 10):
        return await self.qdrant.search(query, limit=limit)

    async def cleanup(self) -> None:
        """Clean up all resources"""
        if self._qdrant:
            await self._qdrant.close()
        if self._db:
            await self._db.close()
        if self._storage:
            await self._storage.close()
        if self._memory:
            await self._memory.cleanup()

Configuration (config.toml example)

[database]
host = "your-postgresql.postgres.database.azure.com"
port = 5432
database = "your-database"
username = "admin"
password = "your-password"
use_azure_ad = false
pool_min_size = 2
pool_max_size = 10
# api_url = "https://your-api.example.com/db"  # Set for HTTP API mode

[database.azure_ad]
tenant_id = "your-tenant-id"
client_id = "your-client-id"
client_secret = "your-client-secret"

[auth.agenticstar]
base_url = "https://auth.example.com"
api_key = ""
timeout = 30.0
max_retries = 3

[rag.embedding]
# provider = "azure" (default): Azure OpenAI deployments path
base_url = "https://your-openai.openai.azure.com/"
api_key = "your-api-key"
model = "text-embedding-3-small"
# dimensions = 1024  # optional (3.1.1+): when set it is sent to the API and the returned length is checked;
#                    # leave it out to use the model's default size (treated as 1536)

# OpenAI-compatible endpoints (e.g. embed-v-4-0 on Azure AI inference) — SDK >= 0.5.24:
# [rag.embedding]
# provider = "openai"
# base_url = "https://your-resource.services.ai.azure.com/models"  # include /models
# api_key = "your-api-key"
# model = "embed-v-4-0"   # "openai/embed-v-4-0" also accepted (prefix is stripped)
# api_version is not used with provider = "openai"; dimensions works as above

[rag.qdrant]
url = "http://localhost:6333"
collection_name = "knowledge_base"
vector_size = 1536

[storage.azure]
bucket_name = "your-container"
connection_string = "DefaultEndpointsProtocol=https;..."
# Without a connection string (Shared Key disabled): leave connection_string = ""
# and set storage_account_name = "yourstorageaccount" (DefaultAzureCredential)
prefix = "uploads/"

[memory.llm]
model = "azure/gpt-4"
api_key = "your-api-key"
base_url = "https://your-openai.openai.azure.com/"
api_version = "2024-02-15-preview"

[memory.embedder]
model = "azure/text-embedding-ada-002"
api_key = "your-api-key"
base_url = "https://your-openai.openai.azure.com/"
api_version = "2024-02-15-preview"

# OpenAI-compatible embedder (e.g. embed-v-4-0 on Azure AI inference) — SDK >= 0.5.24:
# [memory.embedder]
# model = "openai/embed-v-4-0"
# api_key = "your-api-key"
# base_url = "https://your-resource.services.ai.azure.com/models"  # include /models;
#                       # without base_url the client would connect to api.openai.com

API Reference

See the developer portal for the SDK guides and API reference.

Changelog

3.1.3 (2026-10-20) — security: retry Azure requests that were not sent / workflow: the cause chain of an agent failure

  • Azure Content Safety (moderation, Prompt Shield) and PII detection retry a request that was not sent. When the connection fails before the request is sent — the egress proxy refuses CONNECT (httpx.ProxyError, for example the sidecar's 503 while it is still starting), the connection is refused (httpx.ConnectError) or connecting times out (httpx.ConnectTimeout) — the request is sent again after 0.5 s, 1 s and 2 s (3.5 s of waiting in total; each attempt still has the client's own timeout, so a connection that keeps timing out takes up to four timeouts), then the last error is raised as before (SecurityAPIError with error_code="REQUEST_ERROR"). The request never reached Azure, so it is not sent twice. Failures after the request was sent (an error status, a read timeout) are not retried, and the 429 retry of PII detection is unchanged.
  • Why: a Marketplace agent Pod reaches the internet through an Envoy sidecar. For about half a second after the sidecar starts, it cannot connect upstream and answers CONNECT with 503. A workflow agent whose first external call is the input guardrail failed with SecurityAPIError on every attempt; a chat agent was not affected only because it starts more slowly.
  • The workflow child writes the cause chain of a failure to stderr after the class name: agent entrypoint failed: SecurityAPIError [SecurityAPIError(error_code=REQUEST_ERROR) <- ProxyError(host=example.services.ai.azure.com)]. Only class names, the error code of the SDK's own exceptions, the HTTP status and the destination host name are written — no messages, paths or query strings, and no attributes of the agent's own exceptions other than the status. ASTER_WORKFLOW_DEBUG_TRACEBACK=1 still writes the full traceback instead. The completion (error_class) is unchanged.
  • No behaviour change: the default Azure Blob endpoint suffix is now the constant DEFAULT_AZURE_BLOB_ENDPOINT_SUFFIX ("core.windows.net").
  • To use it, rebuild the agent image with this SDK.

3.1.2 (2026-10-15) — workflow: the request of a run started from outside (context.start_input)

  • New WorkflowContext.start_input. When a run is started from outside with a request (a call to the platform's API that carries one, or an email to the project), the first step of the run (after a split, the first step of each lane) can read the request: a read-only mapping {kind, payload, subject, text, external} (see "Reading the request of a run started from outside" in the Workflow Runner section). Other steps get None, and so does a value of the wrong shape (kind not a string or external not a boolean; subject / text that are not strings become None).
  • It is not added to context.manifest (the start record saved to the audit).
  • Platform requirement: the platform runtime API returns start_input in the context (Marketplace 3.2). Against a platform that does not, start_input is always None and nothing else changes. To use it, rebuild the agent image with this SDK.

3.1.1 (2026-10-15) — rag / memory: the embedding dimension can be set / security: PII detection languages

  • The embedding dimension can be set (dimensions). Leaving it out keeps the previous behaviour: EmbeddingConfig.dimensions stays None, the generator treats the size as 1536 (EmbeddingGenerator.dimensions == 1536), nothing is sent to the API and the returned length is not checked. When it is set (EmbeddingConfig(..., dimensions=1024) or "dimensions": 1024 in from_dict), it is sent as the API's dimensions (except when the model or deployment name ends with text-embedding-ada-002, which rejects it; an ada-002 deployment under another name must not set dimensions) and every returned vector must have exactly that length; otherwise EmbeddingDimensionError (a subclass of EmbeddingError) is raised without caching or retrying, so a wrong-size vector is never stored. Use a dimension the model supports (for example Cohere Embed v4 and text-embedding-3-* support shorter vectors). A value that is not a positive integer (a string of ASCII digits is accepted; true, 1.5 or "²" are not) raises ValueError when the config is built, without echoing the value. An unset dimension stays unset when the config is copied (dataclasses.replace, asdict → from_dict). Changing the dimension of an existing collection needs re-indexing; the SDK does not do it.
  • Compatibility: code that already passed dimensions=1536 explicitly — including configs copied from the earlier [rag.embedding] example in this README, which set dimensions = 1536 — now sends dimensions=1536 to the API and checks the returned length. Models that support shorter vectors accept it; for a model or gateway that rejects the parameter, leave dimensions out.
  • QdrantConfig.vector_size must match the generator's dimension, as before (QdrantManager raises QdrantConfigError otherwise). Pass vector_size=generator.dimensions. vector_size=None now means 1536, so QdrantConfig(vector_size=embedding_config.dimensions) keeps working when the dimension is not set. An existing collection is not checked or changed.
  • Mem0 (SemanticMemoryClient): LLMProviderConfig.dimensions (from "dimensions" in from_dict) is passed to convert_embedder_to_mem0 as embedding_dims, and should match embedding_model_dims of the vector store. What happens next is Mem0's own behaviour, which the SDK does not change: the OpenAI-compatible embedder sends it as dimensions, Gemini as output_dimensionality, Bedrock only for Titan v2 models (Cohere and Titan v1 ignore it), the Azure embedder does not send it (send dimensions yourself there); the returned length is not checked, and text-embedding-ada-002 rejects the parameter (do not set dimensions for it).
  • New helpers: parse_embedding_dimensions(value) (exported from agenticstar_platform.rag) and DEFAULT_EMBEDDING_DIMENSIONS (1536).
  • Dependencies: the rag, lab and all extras now require openai>=1.10.0 (the first release whose embeddings API accepts dimensions).
  • PII detection languages: AzureSecurityClient.detect_pii / detect_pii_batch map the product's language codes to Azure AI Language codes before sending (zh-Hans, zh-CN, zh-SG → zh-hans; zh-Hant, zh-TW, zh-HK, zh-MO → zh-hant; pt-BR → pt-BR). Other values are sent unchanged, as before.

3.1.0 (2026-10-14) — db: the proxy's drain answer is retried, idle connections expire before the proxy closes them, extra request headers / storage: Azure Blob without a connection string (DefaultAzureCredential)

The version follows the platform release (3.1.0); it does not mean a breaking change. 3.0.16 was not published: its storage change ships here.

  • ApiPostgreSQLManager (the /db/execute proxy used in CLI and Pod mode) resends the proxy's "not processed" answers. The proxy answers 503 before running the query while a replica is being recycled (detail: cli-api is draining for memory recycle, retry on another replica) and when it cannot check the calling Pod yet (caller binding unavailable (retryable)). Only these exact JSON answers are resent: the SDK waits at least Retry-After (seconds or an HTTP date, capped at 10 s; a missing or unusable value falls back to the backoff) within the same 5-attempt budget as connection failures (waits of 1, 2, 4 and 8 s, about 15 s with the proxy's Retry-After: 1, at most about 40 s). Any other 503 or 5xx, including one from something else on the way, may arrive after the query ran and is still returned as API_ERROR without a retry; so is a resendable answer on the last attempt.
  • Idle connections to the proxy expire after 4 s (httpx keep-alive expiry). The proxy closes idle keep-alive connections after 5 s; reusing one at that moment failed after the request was sent (Server disconnected without sending a response), which cannot be retried safely. The pool sizes stay at httpx's defaults (100 connections, 20 kept alive).
  • New optional headers_provider on ApiPostgreSQLManager(config, token_provider=..., headers_provider=...): a callable returning a mapping of extra headers for each request (for example the caller's execution id, so the proxy can check the caller). Only headers that can be sent are added: the name must be an HTTP token and the value visible ASCII without leading or trailing spaces (a str or int). Headers the client owns (Authorization, Host, Content-Type, Content-Length, ...) cannot be set, and a provider that raises adds nothing. Without it, requests are exactly as before.
  • AzureBlobStorageClient can now sign in without a connection string. AzureBlobConfig.connection_string is optional; when it is absent (or an empty string, which is how a config file says "none"), the client builds the account URL from the new storage_account_name (plus endpoint_suffix, default core.windows.net) and authenticates with DefaultAzureCredential (Workload Identity / Managed Identity on AKS, Azure CLI locally). This is the path to use once a storage account has Shared Key access disabled. The identity needs Storage Blob Data Contributor on the account. A config with neither value raises StorageConfigError at construction, as a missing connection string did before.
  • Compatibility: a config with a connection string behaves exactly as before, including positional construction (AzureBlobConfig("container", "DefaultEndpointsProtocol=...")). str(config) never prints the connection string and now says which of the two it uses. close() also closes the credential.
  • Dependencies: azure-identity>=1.15.0 is added to the storage-azure and storage extras. It is imported only on the credential-less path, so AzureBlobStorageClient still resolves with azure-storage-blob alone.

3.0.15 (2026-10-10) — db: a text parameter that looks like a date is no longer turned into a date by the proxy

  • ApiPostgreSQLManager (the /db/execute proxy used in CLI and Pod mode) now sends param_types next to params: one entry per parameter, "datetime", "date" or None, nested the same way for lists and dicts. The proxy restores only the parameters the SDK marked. Up to 3.0.14 the proxy guessed from the shape of the string, so a plain str such as "2026-10-09" or "2026-10-09T01:02:03" bound to a TEXT column arrived as date / datetime and asyncpg rejected the statement (expected str, got date). Real date / datetime values, including tz-aware ones, keep arriving as before.
  • Compatibility: a proxy that does not know param_types ignores it and keeps guessing as before; a new proxy keeps guessing for an SDK that does not send it. The direct PostgreSQLManager is not involved.

3.0.14 (2026-10-06) — cycle executions: the agent can report whether it completed its goal (report_completion)

  • New report_completion() (from agenticstar_platform import report_completion, also exported from agenticstar_platform.db; requires the [db] extra). A cycle execution (execution_data.runtime_protocol = mp-chat/1) that exits 0 was always recorded as outcome NULL / runner_exited (finished, completion not measured), so cycle agents never counted as a success. The agent can now report one of the 7 saved outcomes (completed / incomplete / failed / timeout / stopped / denied / skipped) with an optional reason code (short snake_case, defaults to the outcome). The platform adopts the outcome and the reason code only when the container exits 0; had_mutating_success and summary are kept in the report row. See section 2.4 for a complete example.
  • Optional and best effort. Agents that do not report behave exactly as before (unmeasured). The report is written once per execution: concurrent calls are serialized with a transaction-scoped advisory lock, and a later call writes nothing and returns False. A database failure (including a 30-second wait for a connection) is logged and returns False instead of raising, so the agent's own work is never failed by reporting. Only an outcome outside the 7 values raises ValueError; a reason that is not a short snake_case code is replaced by the outcome with a warning.
  • The report is one row in execution_messages (content_data.kind = sdk.runner.completion, keyed by CONVERSATION_ID / MESSAGE_ID like the started fence of 3.0.12). No DDL.
  • Platform requirement: the marketplace executor must read the report. Against an older one the row is written and ignored, and the execution stays unmeasured.
  • Nothing else changes: the runner does not report on the agent's behalf, and workflow runs keep using the completion envelope.

3.0.13 (2026-10-06) — workflow: the runtime token is unreadable from the agent's processes / cancellation after start is a stop, not a failure / context.log reaches the pod log

  • The agent can no longer read the runner's token through /proc. The child never received ASTER_RUNTIME_TOKEN, but it runs as the same user, so it could read the runner's /proc/<pid>/environ. run() now marks the runner process non-dumpable (prctl(PR_SET_DUMPABLE, 0), Linux) before starting the agent; the agent's own /proc is unaffected. Start the image in exec form so that the runner is PID 1 (see the workflow section); the runner logs a warning when PID 1 still holds the token. arun() / WorkflowRunner do not change the process unless you pass protect_token=True.
  • Cancellation, a closed run or the deadline after start ends as a stop. After cancellation the platform refuses every write of the run, including audits, artifacts and the completion (409 cancelled / run_closed / deadline_exceeded). Up to 3.0.12 a refused audit counted as an audit persistence failure (failed/audit_persist_failed) and a refused completion ended with exit code 2; the start fence refused before start was logged as an error. They now map to the stop reason as the input fetch already did (stopped/cancelled, timeout/execution_timeout), the agent is not started, and a completion refused because of cancellation exits with 0 (the platform closes the run as stopped regardless). run_closed and deadline_exceeded keep exit code 2 so the platform closes the run without waiting for the collection deadline.
  • context.log reaches the pod log. The SDK configured no log output, so with the recommended start command context.log() and the runner's INFO messages were dropped. run() adds a stderr handler to the agenticstar_platform logger when the process has not configured logging (the root logger and other libraries are not touched).
  • Audit metadata that cannot be sent no longer crashes the runner. NaN / Infinity passed the metadata filter and made the send raise, which ended the runner without a completion. The SDK now drops them, and logs the names of dropped metadata keys (only str / int / finite float / bool are sent; values are not logged). The runner also rejects malformed events written directly to the control channel as unsaved audits instead of failing, and ignores a non-string error_class.
  • run(stop_on_sigterm=...). run(exit_process=False) also turned off the cooperative stop on SIGTERM. The new argument sets it separately (default: the value of exit_process, as before).

3.0.12 (2026-10-06) — runner: a recreated Pod does not run a cycle execution's agent twice (started fence)

  • For cycle executions, run_marketplace_agent / arun_marketplace_agent take a started fence before calling the agent. A cycle execution is one whose execution_data.runtime_protocol is mp-chat/1 (scheduled by the platform; its contract is that an execution is never run again after its Pod is lost). In one short transaction (READ COMMITTED) the runner takes pg_advisory_xact_lock on the execution's (CONVERSATION_ID, MESSAGE_ID), checks execution_messages for any row of that pair, and writes one marker row (chunk_type progress_message, content_data.kind sdk.runner.started). If a row already exists — the Pod was recreated and a previous runner had already started the agent — the runner does not call the agent, sends no events and raises the new MarketplaceRunnerFenceTaken; the process exits non-zero and the platform closes the execution as failed with unknown effects. Up to 3.0.11 the agent could run again in the new Pod before the platform noticed the lost Pod (its reconcile interval, 30 seconds by default), repeating external writes.
  • Fail closed. When the fence cannot be taken (lock wait over 10 seconds, a query or commit failure), the agent is not called, no events are sent (a runner that cannot prove it owns the execution must not send a terminal event — a previous runner may already have sent one) and the error is raised. For a normal conversation whose execution row cannot be read either (a database outage), the failure therefore shows only when the platform closes the execution.
  • Other conversations are unchanged. For a normal conversation (the execution row says it is not a cycle execution) the runner does not look at or write the marker, so a container that restarts after a crash runs the agent again as before. When the execution row cannot be read, the runner treats the execution as a cycle execution and takes the fence (the first run of a normal conversation still passes it); with a data_access that has no pool it behaves as before.
  • New started_fence argument (default True). The fence uses the direct PostgreSQL pool (PostgreSQLManager.pool). For a cycle execution with a data_access that has no pool (HTTP-API mode or your own implementation), the runner raises MarketplaceRunnerConfigError unless you pass started_fence=False; then a recreated Pod may run the agent again.
  • Admin dashboards that aggregate execution_messages count the marker as one chunk of the execution (its start time becomes the marker's time). The execution trace, which is built from the run record, does not show it.
  • Rebuild your agent image with 3.0.12 to get the fence. Images built with older versions keep the previous behavior.

3.0.11 (2026-10-06) — policy: which rule layer decided a forbid (Decision.layer)

  • New Decision.layer. For a forbid it is project, tenant, floor or system (constants LAYER_PROJECT / LAYER_TENANT / LAYER_FLOOR / LAYER_SYSTEM); for an allow it is None. A forbid is project only when the project's settings alone can lift it: a project deny rule that also hits a tenant or system stop below it reports that lower layer (reason and matched stay those of the deny rule). Use it to tell the user whether the project's settings can change the result: for example, a destructive operation forbidden by profile is tenant when the tenant's default profile is guarded, and project when the project chose guarded itself. Up to 3.0.10 the reason code alone could not tell these apart.
  • mode, reason and matched do not change. Code that compares whole Decision objects with == must include layer (a forbid without a policy is now Decision("forbid", "policy_unavailable", 0, layer="system")).

3.0.10 (2026-10-04) — auth: refresh an MCP token that was rejected before its expiry

  • New rejected_token_sha256 argument on AgenticStarAuthClient.get_mcp_tokens(). A mapping of provider → SHA-256 of an access token that the MCP server rejected with 401 (hex, lowercase, 64 characters; hash the token string as UTF-8). When the stored token is still that token, the token supply refreshes it even if it has not expired yet; when it has already been replaced, the newer token is returned. Do not send the token itself.
  • Some providers revoke tokens before the stored expiry (Slack keeps at most two valid tokens per installation; some providers revoke the previous token when a new one is issued). Up to 3.0.9 the same rejected token came back until its expiry, so the MCP server stayed unusable.
  • The supply does not refresh personal access tokens, connections without a refresh token or whose provider does not support refresh, or tokens issued less than a minute ago; in those cases it returns the stored token as before.
  • Platform requirement: the token supply (chatboardlogin) must support the field. Against an older one the field is ignored and nothing else changes. Without the argument, requests are the same as before.

3.0.9 (2026-10-03) — workflow: the review decision that started a run (context.gate_decision)

  • New WorkflowContext.gate_decision. When a step runs again because a reviewer sent it back, or the next step runs after a reviewer approved, it is a read-only mapping {gate_id, decision, decided_by, note, decided_at}:

    • decision: send_back or proceed.
    • decided_by: human, or system when the decision was made automatically (after the deadline, or on an external condition).
    • note: the reason for sending back, or the comment added on approval, as written by the reviewer. None when the reviewer wrote nothing or the decision was automatic.
    • decided_at: when the decision was made (ISO 8601).

    It is None for every other run (the first step of a case, steps that do not follow a review, and platforms that do not return it). Up to 3.0.8 the reason never reached the agent, so a step that was sent back produced the same output again; use note to tell your agent what to fix.

  • context.manifest (the start record saved to the audit) includes gate_decision with gate_id / decision / decided_by when the run follows a review. The reason / comment text is not included.

  • The sample docs/examples/workflow_reply_agent.py adds the reason to its instructions after a send-back.

  • Platform requirement: the platform runtime API returns gate_decision in the context. Against a platform that does not, gate_decision is always None and nothing else changes. To use it, rebuild the agent image with this SDK.

3.0.8 (2026-10-01) — security: Prompt Shield inspects documents to the end

  • AzureSecurityClient.check_prompt_shield(documents=...) now inspects every document to the end. Documents are split into overlapping windows in the same way as user_prompt, packed into calls of at most 9,500 bytes and 5 documents each, and the results are combined (an attack in any window is reported). Up to 3.0.7, all documents together were cut to their first 9,500 bytes and sent only with the first call, without a warning, so an indirect prompt injection placed later in external content (after roughly 3,100 characters of Japanese text) was not inspected.
  • When documents are longer than user_prompt, the remaining calls carry the first window of user_prompt (the API requires it). A short prompt with short documents is still checked in one call.
  • The inspection budget per document is the same as for user_prompt (AGENTICSTAR_AZURE_CS_MAX_WINDOWS, 8 windows by default); a document longer than that logs a warning, as user_prompt already does.

3.0.7 (2026-10-01) — documentation: this README is now entirely in English

  • This README and the documents bundled in the package (the Local Integration Lab README agenticstar_platform/lab/README.md, the comments in the lab's docker-compose.yml, and the audit ROW_HASH_SPEC.md) are now written entirely in English, and the changelog has been rewritten for external readers.
  • The Policy Module example now reflects the MCP behavior introduced in 3.0.3 (MCP calls are allowed unless the floor or a forbid rule matches; the strict allowlist applies to built-in tools), and the Modules table lists metering / runner / audit / policy / workflow.
  • The sample agents under docs/examples/ (source distribution) are now in English, and two ValueError messages raised by GuardrailAlertWriter no longer contain internal references.
  • No behavior changes from 3.0.6. Upgrading is optional.

3.0.6 (2026-09-30) — workflow: the runner reports a known stop in every reply / a closed control channel raises WorkflowError

  • The runner now attaches any stop it already knows about (cancellation, deadline, terminal state or a pod stop signal) as stop_reason to its reply to every child request (audit, log, skip, check, connection_token, …). The child latches the stop as soon as it reads it in its IPC receive loop — even from a reply to a request whose caller was cancelled. From then on is_cancel_requested() returns True without IPC, and ensure_can_act() / connection_token() raise WorkflowError. Up to 3.0.5, replies to audit and other requests did not carry the stop, so the child only learned about it at the next cancellation check (every 5 seconds by default).
  • A failed IPC write in the child (BrokenPipeError / OSError / ValueError on a closed pipe) now raises WorkflowError (runner closed the control channel). An agent that catches WorkflowError no longer misses audit or cancellation calls made after the runner has closed the control channel.
  • Only stops the runner has already determined are attached (building a reply never newly determines a stop signal or deadline). Replies when no stop is known, and how the agent's outcome is decided (including the boundary right after done), are unchanged from 3.0.5.

3.0.5 (2026-09-30) — workflow: refresh connection access tokens during a run (context.connection_token) / cancellation during input staging no longer crashes / auth: get_user path-encodes user_id

  • New WorkflowContext.connection_token(slot, *, min_ttl=300.0). Returns {access_token, token_type, expires_at} as a read-only mapping (it contains a token — never put it in logs or exception messages). OAuth access tokens for connections passed through the environment at startup can expire after about an hour; use this before calling external APIs in long runs.
    • The cached value is returned until less than min_ttl seconds remain before expiry; after that it is refreshed through the runner from the platform runtime API (GET /api/mp/runtime/v1/executions/{id}/connections/{slot}/token). Tokens with an unknown expiry (expires_at is null) are refreshed every UNKNOWN_EXPIRY_REFRESH_SECONDS (600 seconds).
    • Raises WorkflowError for: a slot not bound at startup; a connection the platform reports as revoked or awaiting re-authorization (the old cached value is not returned either); any call after a cancellation, deadline or terminal state known to the runner (cached values are no longer returned); and a refreshed token that is already expired (the old cached value is not returned either).
    • Right before handing out a token (after the last wait), the child re-checks stops it knows about, the agent deadline, whether a concurrent refresh was refused in the meantime, and the token's own expiry. Concurrent calls are processed one at a time, and if a caller is cancelled the result of its refresh (discarding the cached value / latching a stop) is still applied. Calls cancelled while waiting their turn do not refresh (including a cancellation right after the lock is released). If another call refreshed successfully in the meantime the new value is returned; if that refresh was refused, WorkflowError is raised. A cached value that dropped below min_ttl while waiting is refreshed.
    • On the cached-value path, cancellation can be noticed up to about 10 seconds late (the child's and the runner's check intervals, 5 seconds each by default). The runner checks with an interval before the first fetch, and after it always asks the platform for cancellation / terminal state (if the platform cannot be reached, it decides from locally known stops and deadlines only). For connection_token and cancellation checks (check) it re-checks stop signals and deadlines even after waiting. The platform's refresh API itself rejects requests with 409 after cancellation, run termination or the deadline.
    • Added RuntimeClient.get_connection_token(slot) and the runner IPC op connection_token. Fetching a token and the cancellation check after it are each attempted at most twice (the runner's IPC is serial, so the agent's audit / cancellation calls wait meanwhile). Slots that do not match the platform's format (a letter followed by letters, digits, _ or -, up to 64 characters) are rejected without sending a request (. / .. would reach a different API path after URL normalization). Token values are never logged.
    • Platform requirement: the refresh API must be available on the platform. Against a platform without it, the call raises WorkflowError (connection token unavailable). The tokens passed through the environment at startup and the existing APIs are unchanged (for agents that never call connection_token, only the two changes below apply).
  • WorkflowContext.is_cancel_requested() / ensure_can_act(): a stop that arrives while the caller is cancelled mid-check is still recorded (subsequent calls return True). The runner's cancellation check (IPC op check) now also returns stop signals / deadlines that arrived while it was waiting (up to 3.0.4 they were noticed only at the next check).
  • runner: if the platform returns 409 cancelled / run_closed / deadline_exceeded while input artifacts are being staged, the runner maps it to a stop reason and sends a completion instead of crashing (stopped/cancelled; a deadline becomes timeout/execution_timeout). The agent is not started (had_mutating_success is false). After a cancellation or deadline the platform refuses to save, so the runner exits with code 2 and the platform decides the terminal state. The platform rejects input fetches after cancellation, run termination or the deadline. Up to 3.0.4 the runner exited with an exception on this 409 (for a cancellation the platform still closed the run as stopped/cancelled). Other API errors (unreachable, 5xx after retries, unexpected 4xx) still raise as before.
  • AgenticStarAuthClient.get_user(user_id): user_id is URL-encoded as a single path segment (values containing / or ? can no longer reach a different API path). . and .. are rejected with INVALID_PARAMETER.

3.0.4 (2026-09-23) — policy: forbid external A2A agents by connection ID (schema 4, connection_kind)

  • External forbid rules gain a connection kind, connection_kind (mcp | a2a). Omitted = mcp (the same stored form and digest as 3.0.3; to_dict() does not emit connection_kind for mcp). An a2a rule is {scope: "external", connection_kind: "a2a", connection_id: <A2A connection ID>} and has no tool / method (an A2A agent receives messages and a skill cannot be targeted, so no rule is created at a granularity that cannot be enforced).
  • New evaluate(..., connection_kind="a2a", connection_id=...). Like MCP, sending to an A2A agent is outside the execution rules (profile / dry_run / delegation / operation class): unless it matches the floor or an explicit forbid rule (the A2A connection ID, or an unscoped tool-name glob) it is allow (reason a2a_unrestricted). If a2a rules exist but the connection cannot be identified (connection_id is None), the call is unevaluable (forbid). MCP rules never apply to A2A and vice versa (the integer IDs come from different registries, so kind and ID are matched as a pair). Unknown kinds are forbidden.
  • A policy containing a2a rules is schema_version 4 (SCHEMA_VERSION_TYPED_CONNECTION); with mcp rules only it stays 3, with unscoped rules only it stays 2. Older readers (≤ 3.0.3) reject 4, so the policy cannot be loaded and everything is forbidden (an A2A forbid is never silently dropped).
  • verify_connections(policy, connections) accepts a per-kind map {"mcp": {...}, "a2a": {...}}. A flat map / set is read as mcp (3.0.3 callers are unchanged). It raises ValueError if a2a rules exist but there is no a2a map, or a rule references a connection that does not exist.
  • Callers must pass connection_kind="a2a" and the A2A connection ID when evaluating calls to A2A agents. Callers that do not are evaluated as before (the A2A call is treated as an unregistered built-in tool).

3.0.3 (2026-09-22) — policy: MCP tools are not classified by operation class (outside the execution rules)

  • evaluate(..., mcp=True) returns allow (reason mcp_unrestricted) unless the call matches the floor or an explicit forbid rule (an external connection_id + method glob, or an unscoped mcp:<name>:<tool> glob). The profile operation-class table (open / guarded / strict), dry_run, delegated and the unclassified (unknown_tool) stage do not apply to MCP. An op_class passed by the caller is ignored (floor entries conditioned on op_class do not apply to MCP). unregistered is not set (having no classification is normal).
  • Unchanged: no policy = forbid; the order of floor / forbid-rule matching; an MCP call without connection_id while external rules exist = unevaluable (forbid); evaluation of built-in tools (mcp=False) is the same as 3.0.2.
  • Rationale: MCP tool names and meanings are defined freely by each server, and no naming convention, server self-declaration or manual administration can keep a read / write / destructive classification correct over time. Using such a classification as a control input blocks tools nobody forbade (for example, every unclassified tool under strict). MCP is now "allowed by default; forbid specific tools with explicit forbid rules".
  • Impact: in strict / guarded / dry_run projects, MCP calls that used to stop at unknown_tool / profile / strict_allowlist / dry_run_read_only now pass. Put MCP methods you want to block into an external forbid rule (connection ID + glob) or the floor. Audit reason codes gain mcp_unrestricted.

3.0.2 (2026-09-22) — policy: external forbid rules are matched by connection ID, not name / db: id in MCP configurations

  • New evaluate(..., connection_id=): the identifier of the MCP connection (the string form of its configuration ID). External rules compare it with the rule's connection_id. mcp_server remains the connection's display name (used for the mcp:<name>:<tool> candidates in allow / unscoped forbid globs, never for identification). For an MCP call (mcp_server set) with connection_id None, the connection cannot be identified, so if any external rule exists the call is unevaluable (forbid with unevaluable_args; the name is never used as a substitute). In-process MCP servers that are not registered connections pass LOCAL_CONNECTION ("local") and are not subject to external rules. mcp: bool | None (added at the same time): the caller states explicitly whether the call is MCP. Enforcing callers must always set it (with mcp=True, a call with an empty or blank name and no ID is also an unidentifiable MCP call = forbid; only legacy callers that omit mcp keep the old inference from a name or ID).
  • Breaking: bind_connections() removed. Its replacement, verify_connections(policy, connections), only checks that the connections referenced by rules exist (map / set; int or str keys) and raises ValueError otherwise. The policy is not modified.
  • ForbidPattern.server (the bound name label) removed. The stored form of an external rule is now {scope, connection_id, tool} only, and snapshots up to 3.0.1 that contain server are read with it discarded, yielding the same stored form and digest (digests of policies containing external rules change; upgrade the components that resolve and evaluate policies in the same release). server on an internal rule still raises ValueError.
  • Rationale: connection names are free-form display strings set by administrators, and differences such as surrounding whitespace or __ made rules match or miss the wrong connection. Instead of restricting the characters allowed in names, identification moved from the name to the ID.
  • ConfigAccess.get_mcp_configurations() now includes "id" (the MCP server configuration ID) in each returned item.

3.0.1 (2026-09-21) — workflow: outcome vocabulary rebuilt around how the run ended (envelope version 2) / audit: verification-only vocabulary removed

  • Outcome vocabulary: acted / noop / degraded replaced by completed / incomplete / failed / timeout / stopped / denied / skipped. Success means the agent ran to completion; the success of writes, unobserved effects and zero observations do not decide the outcome. If the agent function returns normally the outcome is completed (reason code completed). The old degraded/effects_unobserved / effects_partial_failure / no_observation / failed/effect_failed no longer exist.
  • Whether a retry is safe is decided by whether the effect is unknown, not by the outcome: the third return value of decide_outcome() and the envelope's had_mutating_success are now Optional[bool]. Audit persistence failures, writes with no effect or with an unknown status, a runner restart / stop / incomplete cleanup, and termination elsewhere yield None (unknown) unless a success was observed. The platform does not automatically retry unsuccessful runs with None, or the reason codes the runner could not fully observe (runner_restarted / runner_stopped / cleanup_uncertain / audit_persist_failed / terminal_elsewhere).
  • New values for the old degraded/... terminals: cancelled → stopped/cancelled, terminal_elsewhere → stopped/terminal_elsewhere, runner_stopped / runner_restarted / cleanup_uncertain / audit_persist_failed → failed/<same reason code>.
  • build_envelope(): version / envelope_version are 2. The gate_requested (always null) and verification fields are removed; how the runner itself ended (stop reason / restart / incomplete cleanup) is carried in runner (the argument is renamed from verification to runner).
  • decide_outcome() gains effects_uncertain (the runner could not fully observe the agent's result).
  • New reason code failed/skip_after_effect: a skip before external actions (policy_unavailable) is not accepted once any mutating tool has been invoked (even if that write succeeded; checked before missing required outputs), because skipped/policy_unavailable makes the platform re-check the policy and run again. The platform routes this reason code to a human as effect-unknown.
  • Breaking: counts keys renamed: tools_invoked → tool_calls, reads → read_calls (the same keys the platform's execution records and exports read). mutating_success / mutating_failed are unchanged, and so are the SDK-only observations (mutating_invoked / mutating_unresolved / orphan_effects / effect_conflicts / denied / audit_persist_failures). Consumers that read counts.tools_invoked / counts.reads from stored envelopes must be updated.
  • workflow exports ENVELOPE_VERSION (= 2).
  • verification.completed removed from audit.KNOWN_EVENT_TYPES (the vocabulary is descriptive; writers do not enforce it).
  • No backward-compatibility shim is provided. The platform's completion API accepts only version: 2, so upgrade together with the platform.

3.0.0 (2026-09-21) — version number aligned with the product version / metering: prompt-cache reads and writes recorded separately

  • Version number: from this release the SDK version follows the AGENTIC STAR product version as 3.0.x (3.0.1, 3.0.2, … from here on). It is the next public release after 1.0.3. The number was chosen to align with the product, not as a semver major bump. It does, however, contain changes incompatible with 1.0.3: the three removals from the unreleased 1.0.4 (see 1.0.4 below: items_advanced / no_items / claim.stale) are included. Check that entry when upgrading from 1.0.x.
  • llm_usage_ledger gains cache_read_tokens / cache_creation_tokens (cached_tokens is kept with the same meaning). Cache reads (0.1×) and cache writes (1.25×) differ in unit price by 12.5×, and a combined number cannot detect a cache that is written but never read. The daily rollup table llm_usage_daily and its function are unchanged (aggregate the breakdown from the ledger by date).
  • extract_usage() returns the breakdown as cache_read_tokens / cache_creation_tokens. Explicit values (including 0) take precedence; otherwise the previously resolved cached_tokens (by convention: reads; including provider details / cache_read_input_tokens) and cache_creation_input_tokens are used as-is. If they cannot be obtained the keys are omitted (not filled with 0). Values that cannot be read as numbers are treated as unknown without raising (extract_usage() returns the raw value, the ledger stores NULL, and the usage-based fallback in default_cost_fn() returns None).
  • Callers that put reads + writes combined into cached_tokens should pass the breakdown explicitly. The default_cost_fn() fallback bills with the explicit values when present (billing a combined number as reads would count the writes at 0.1× and undercount the cost). For existing usage without explicit values, extraction and cost are unchanged.
  • Existing tables: the record() INSERT assumes both columns. ensure_schema() adds them through LEDGER_MIGRATIONS (idempotent). Environments where a migration already added the columns do not need ensure_schema(). If you use neither, apply the same ALTER TABLE … ADD COLUMN IF NOT EXISTS before upgrading to 3.0.0 (as when missing_reason was added). If your DB manager reports failures as a return value (success=False), neither ensure_schema() nor record() turns that into an exception (as before; with the default fail_safe=True, record() also suppresses exceptions), so confirm that both columns exist before upgrading writers to 3.0.0, and confirm that rows are being written to the ledger afterwards. The breakdown is NULL for past rows and for rows written by SDKs older than 3.0.0 (it cannot be recovered from the combined value).

1.0.4 (2026-09-17, unreleased; included in 3.0.0) — workflow / audit: work-item mechanism removed

  • items_advanced removed from the output of workflow.build_envelope() (other envelope keys are unchanged).
  • no_items removed from workflow.envelope.SKIP_REASONS (context.skip("no_items") raises ValueError; for a step with nothing to process use no_target / not_applicable / nothing_to_do).
  • claim.stale removed from the audit vocabulary. KNOWN_EVENT_TYPES is descriptive and writers do not enforce it, so only code that references or enumerates the dict directly breaks.
  • No backward-compatibility shim is provided; consumers of 1.0.3 that reference any of these three must be updated when moving to 3.0.0.

1.0.3 (2026-09-14) — policy: internal tool names removed from public operation-kind descriptions

  • The public DTO returned by internal_operation_kinds() (for admin / help screens) now writes description_ja / description_en in terms of operations and no longer names internal tools. Kind IDs, labels, mapping_version, the full internal mapping (internal_operation_mapping() / kind_tools()) and command_exec covering shell execution only are unchanged.
  • No change to runtime evaluation (evaluate / digest / stored form schema 3). 1.0.2 consumers keep working (updating the pin is optional).

1.0.2 (2026-09-14) — policy: forbidden operations split into internal operation kinds (internal) and connection methods (external)

  • ForbidPattern.scope. internal = a kind ID (kind) + mapping_version, matching an exact set of built-in tool names (never an MCP tool with the same name). external = connection_id + a method glob. (In 1.0.2 a resolver bound server labels with bind_connections(policy, {id: name}); removed in 3.0.2 — rules are now matched by ID through evaluate(connection_id=).) When matching an external rule, an unbound connection or an MCP method name containing : is unevaluable and forbidden (fail-closed).
  • schema_version: only policies containing scoped rules are 3 (SCHEMA_VERSION_SCOPED; otherwise 2). requires_schema() / SUPPORTED_SCHEMA_VERSIONS are public. EffectivePolicy.from_dict() in 1.0.1 and earlier rejects schema 3 with ValueError (callers treat it as "policy unavailable" and forbid everything), so issuers of scoped rules must confirm that consumers are ≥ 1.0.2 first. A scoped stored form that claims schema 2 is rejected.
  • New module agenticstar_platform.policy.internal_operations (kind table v1: text_write / file_read / file_transfer / command_exec (shell execution only) / browser / desktop): internal_operation_kinds() (public DTO: ID / label / description / version only), internal_operation_mapping() (the full internal mapping), kind_tools(), and agenticstar_platform.policy.INTERNAL_OPERATIONS_MAPPING_VERSION (MAPPING_VERSION inside the module).
  • evaluate: surrounding whitespace is stripped from mcp_server and an empty string is normalized to None. Apart from that normalization, matching of legacy bare / glob rules is unchanged; legacy digests and retain_rationale_body are unchanged.

1.0.1 (2026-09-13) — Marketplace workflow runner (mp-workflow/1)

  • New module agenticstar_platform.workflow: run(entrypoint) / arun / WorkflowContext / EffectTally / the completion envelope. Runs only on the environment the platform executor injects (no fallback to DB settings); HTTP transport for the platform runtime API (context / artifacts / audit / completion); runs the agent in a child process with stdio IPC (audit acknowledgement gate, cancellation / deadline checks); safe staging and collection under /workspace; resends under the same completion_id.
  • Safe collection: the root directory is pinned by file descriptor before the agent starts, and only bytes verified through a validated descriptor (O_NOFOLLOW, regular file, link count, size limit) are saved. JSON / YAML are parsed strictly (aliases, anchors, custom tags and duplicate keys are rejected; no implicit yes/no or timestamp conversion; YAML 1.2 core numbers) and validated against the contract schema.
  • Restart safety: started is written to a state file before the agent starts, so a runner that dies before saving the completion never runs the agent again (it resends the saved payload or reports a runner restart). Ownership of an execution is decided on the server side through a fixed manifest event; a runner that cannot prove ownership starts nothing and sends no completion. Acknowledged observations are journaled durably, so a restart never resets counts. Leftover agent process groups are stopped only when ownership can be proven; otherwise the runner reports incomplete cleanup and the run is held for review.
  • Deadlines: if the platform has not set a deadline yet, a provisional one is applied locally (the contract maximum for the execution); a confirmed deadline from the platform replaces a provisional value (earlier or later), and confirmed deadlines can afterwards only get shorter. The agent stops at the earlier of the execution deadline and the collection deadline minus the margin for stopping, collecting and saving, so the contract's execution window is preserved when the collection grace period is long enough. Save requests are bounded by the remaining budget, with 10 seconds reserved for the completion. The child's deadline stop is a watchdog independent of HTTP.
  • Exit codes and failures: the SDK does not guarantee a zero exit code. Failures that prevent sending a completion (unexpected exceptions, cancellation, a broken workspace, input fetch failures, a start fence that could not be made durable) end with the exception and no completion; the platform records a non-zero exit without a saved terminal as effect-unknown and holds it for human review (an exit 0 without a saved terminal is also held after the collection grace period). Only SIGTERM (pod stop) is converted into a cooperative stop (stop the child processes → collect → report the runner as stopped); only run(), the entry point that owns the process, installs the signal handler (arun / WorkflowRunner alone do not). A 2xx runtime API response whose body is not JSON is normalized to RuntimeApiError (for the completion only, it is treated as saved).
  • Only 2xx counts as success for runtime API calls (a 302 + HTML from an ingress or SSO is never mistaken for a save).
  • Adds pyyaml>=6.0 to the core dependencies.
  • The existing run_marketplace_agent (Marketplace chat) API and event delivery are unchanged.
  • The outcome vocabulary of this release (acted / noop / degraded) was replaced in 3.0.1.

1.0.0 (2026-09-13) — 3.0 GA (1.0.0a3 + 0.5.41)

  • Final release combining 1.0.0a3 (policy l1_profile / schema 2, the new audit columns, and ensure_schema support for them) with the 0.5.41 fix to result-set detection in PostgreSQLManager.execute_query() (rows from WITH … SELECT / VALUES / TABLE were lost). No API changes from 1.0.0a3 (policy stored form = schema 2, digest compatible).

1.0.0a3 (2026-09-09) — policy: a project allowlist never widens what the tenant permits

  • EffectivePolicy gains l1_profile (the tenant L1 default profile) and schema_version is raised to 2. to_dict() includes l1_profile; from_dict() requires it and rejects stored forms whose profile is looser than l1_profile (the 1.0.0a2 stored form, schema 1, can no longer be read — fail-closed). Digests are computed over the new stored form, so keep both sides (the one that resolves policies and the one that evaluates them) on the same version.
  • evaluate(): a strict allowlist applies only to operation classes the L1 profile itself does not forbid. Under an L1 guarded tenant (destructive forbidden), a strict project with an allow entry still gets forbid (reason profile) for destructive. The permitted set with a project applied is a subset of L1's own permitted set (an L1 strict allowlist is L1's own mechanism, so it applies to both write and destructive).
  • Exports profile_permits(profile, op_class) (whether a profile alone can permit an operation class).
  • Upgrade procedure (incompatible): while the resolving side and the evaluating side run different versions, neither can read the other's stored form and both fail closed (every new run is forbidden). Switch over by pausing new runs → letting in-flight runs finish → switching all components at once → resuming; never upgrade one side ahead of the other.

1.0.0a2 (2026-09-07) — public surface cleanup

  • ActionAudit(source=...): source is "the name of the component writing the ledger" (any string); assumptions about specific deployment names were removed from error messages and docstrings.
  • Tables created by ensure_schema() include the four columns added in 1.0.0a1 (actor_kind / project_id / root_execution_id / source_event_id) and their indexes (a partial UNIQUE index on source_event_id, project_id, (root_execution_id, occurred_at, id)). Tables created by older SDKs get the columns through ACTION_AUDIT_UPGRADE_DDL (ADD COLUMN IF NOT EXISTS), and writers that had fallen back to the legacy INSERT switch back to the new one. Call it once with the owner role when provisioning (runtime writers need only INSERT). In migration-managed environments, migrations remain authoritative.
  • Adds KNOWN_EVENT_TYPES (known event_type names and their meanings; descriptive, not enforced). Exports ACTION_AUDIT_DDL / ACTION_AUDIT_INDEXES / ACTION_AUDIT_UPGRADE_DDL.
  • The agenticstar_platform.policy documentation was rewritten in product terms, and the guarantees of tighten_merge are stated precisely (the floor cannot be removed and the profile cannot be loosened; a strict allowlist permits the tools it lists). Added the Policy Module section to this README.

1.0.0a1 (2026-09-07) — 3.0 alpha

  • audit
    • Writers fill the new agent_action_audit columns: actor_kind (closed vocabulary agent / human / system / human_external / unknown; mapped from actor_type when omitted), project_id (UUID), root_execution_id (text) and source_event_id (deduplication key, ^[A-Za-z0-9_.:@+-]{1,160}$). Existing calls that pass only actor_type keep working.
    • Agent rows (actor_kind='agent') without actor_ref get the value of ActionAudit(agent_ref=...) (default agent:<source>). Human / system decision events (approval.* plus gate.resolved / content.deleted / forensic.* / policy.changed / item.quarantine_released / workitem.*) require actor_ref (ValueError otherwise).
    • Older schemas: on a database without the new columns, the writer detects UndefinedColumn once and falls back to the legacy 19-column INSERT (shipping the new SDK first never loses audit rows; after the DDL is applied, a restart returns to the new columns).
    • reference_keys(**refs): builds correlation keys for metadata (project_id / run_id / step_id / item_id / tool_call_id / root_execution_id).
    • Numbers in metadata other than integers with an absolute value below 2^53 (floats, larger integers, NaN / Inf) are stored as strings, so implementations in other languages compute the same row hash. Circular references in metadata no longer fail the write (the metadata is replaced with {"unserializable": true} and the row is kept).
    • The row_hash canonicalization is specified in ROW_HASH_SPEC.md, with a fixture (row_hash_fixture.json) for implementations in other languages. The hash rule itself is unchanged (new columns are excluded when None, so existing rows hash the same).
  • New agenticstar_platform.policy: tighten_merge(L1, project) (two layers; the floor cannot be removed; the stricter profile wins), EffectivePolicy (to_dict / from_dict / digest) and evaluate() (precedence: policy unavailable → forbid / floor or explicit forbid / unevaluable / unknown tool (open and guarded allow with unregistered; strict, delegated and dry-run forbid) / classified → open allows everything, guarded forbids destructive, strict uses the allowlist).
  • Pre-release versioning (ended with 1.0.0 GA): the 3.0 SDK was first published as pre-releases (1.0.0aN). pip / uv do not select pre-releases for range specifiers such as >=0.5 while a final release exists, so existing installs did not pick up an alpha automatically; installing one required an exact pin (==1.0.0a1, no --pre needed). 1.0.0 (2026-09-13) is a final release, so range specifiers select it; exact pins (==1.0.0) are recommended.

0.5.41 (2026-09-09)

  • Fix: PostgreSQLManager.execute_query() silently dropped the rows of WITH ... SELECT / VALUES / TABLE — whether a statement returns a result set was decided from the query text (starts with SELECT / contains RETURNING), so row-returning statements that did not start with SELECT went through conn.execute() and lost their rows without an error (e.g. {"success": True, "data": {"result": "SELECT 1"}}). Detection now uses PostgreSQL's Describe result (PreparedStatement.get_attributes(), or whether rows came back): statements with a result set return List[Dict] regardless of their first keyword. A zero-column result set (SELECT FROM t) with rows is kept as [{}, ...] (a zero-column, zero-row result cannot be told apart from no result set and returns {"result": "SELECT 0"} — a known edge case; return at least one column to get a normal []).
    • Backward compatibility: statements without a result set (INSERT/UPDATE/DELETE/DDL) still return {"result": "<status>"}. For ;-separated multiple statements, only the path that previously went through the simple protocol (no parameters, not starting with SELECT, no RETURNING) still executes and returns a status dict; other multi-statement queries (SELECT 1; SELECT 2, or with parameters) still fail with DB_QUERY_ERROR as before. Multiple statements never return rows (a warning is logged when one looks row-returning); call one statement at a time if you need rows.
    • The implementation uses an unnamed prepared statement (prepare(query, name="")), the same path as the previous conn.fetch() on asyncpg 0.29/0.30 (no named statements are left behind, even with PgBouncer transaction pooling). command_timeout is shared between prepare and fetch, so the budget is still one query.
    • Behavior difference: a data-modifying CTE without RETURNING (WITH ... INSERT ...) used to return [] depending on whether the text contained RETURNING; it now returns a status dict because it has no result set. Misdetection caused by column names or comments containing RETURNING is gone.
    • DataAccess.execute_query() delegates to this method, as does the platform's HTTP DB API that ApiPostgreSQLManager calls, so both are fixed as well.

0.5.40 (2026-09-01)

  • Guardrail alerts: finer classification
    • SOURCE_SURFACES gains agent_worker (for producers on the agent runtime surface), accepted by both the row writer and the aggregates writer.
    • guardrail_alerts gains a policy_category column (closed vocabulary) — the attack-method classification of the regex / LLM semantic layers (prompt_injection / system_prompt_extraction / implementation_access / credential_request / file_system_access / security_bypass / custom_policy / unclassified). GuardrailAlertWriter rejects values outside the vocabulary synchronously with ValueError (so free text and rule names cannot flow in). Adding the column and replacing the CHECK constraint on existing deployments is the job of your database migrations (the ensure_schema DDL applies only when the table is created).
    • The recursive SelfHarm guard now also scans prompt_shield_origin / policy_category.
  • Security: PromptShieldResult gains user_prompt_attack / documents_attack — the breakdown by attack path, direct (userPrompt) / indirect (documents). Previously these were OR-ed across windows, leaving no way to fill guardrail_alerts.prompt_shield_origin. Both are optional (None = not available, distinct from False = evaluated and not detected). attack_detected is unchanged (backward compatible).

0.5.39 (2026-08-27)

  • Fix: the metadata fallback check for tools_used compared raw values (follow-up to 0.5.38) — the check used !=, which invoked the raw value's __eq__; for types with ambiguous truthiness, such as numpy arrays, this raised and could fail the whole telemetry write. The check is now type-based (a plain int that fit into the column) and never compares raw values.
  • Fix: the metadata fallback used setdefault, so an existing metadata['tools_used'] caused the top-level raw value to be lost — aligned with the behavior up to 0.5.37, where an unknown field overwrites the metadata key of the same name.
  • Docs: removed the overstatement "invalid values never fail the write" from the known-columns section. What is guaranteed is that the column value never makes the INSERT fail; a raw value kept in metadata follows the same JSON-serializability rules as any other metadata field.

0.5.38 contains the two issues above, so 0.5.39 or later is recommended (storing into the tools_used column itself works in 0.5.38).

0.5.38 (2026-08-27)

  • Fix: ai_telemetry.tools_used fell through to metadata instead of its named column — TelemetryAccess.save_telemetry() did not list tools_used in known_fields / the INSERT, so the tool count went to jsonb and the column was always NULL (anything reading the column always showed it empty). It is now stored directly in the (integer) column and included in the SELECT of list_telemetry().
    • Type contract: accepts int / list / tuple; a list or tuple is normalized to its length (the column is an int whether callers pass a count or a list).
    • Not measured is NULL; measured and zero is 0. bool, negative values, values outside the PostgreSQL integer range and uninterpretable types leave the column NULL (an out-of-range value never makes the INSERT fail and lose the row).
    • Backward compatibility: when the column cannot fully represent the raw value (a list breakdown, a wrong type, out of range), the raw value is kept in metadata.tools_used, so the information that used to go to metadata is not lost. A plain int is fully represented by the column and is not duplicated into metadata.
    • Past rows are not backfilled.
    • ⚠️ For readers of this column: the value is the number of tools used (integer), not a comma-separated list of tool names, and runs that used no tools return 0, not null (null means not measured).
    • Added the list of known columns (reserved field names) to the Telemetry section of this README.

0.5.37 (2026-08-27)

  • Fix: the OpenAI-compatible LLM conversion keeps an explicit base_url as Mem0's openai_base_url — the openai branch of convert_llm_to_mem0() dropped base_url, so even with a private gateway configured, Mem0's fallback could send memory adds to the official api.openai.com (broken memory, and credentials sent to the wrong endpoint). It now follows the same contract as the embedder conversion (fixed in 0.5.24). Without base_url, the official OpenAI fallback and the other provider branches are unchanged.

0.5.36 (2026-08-26)

  • Fix: the duplicated LLM / embedder conversion logic in memory was unified into module functions — SemanticMemoryClient's _normalize_provider / _parse_model_string / _get_api_key / _convert_llm_to_mem0 / _convert_embedder_to_mem0 now delegate to the module functions of the same name. This fixes two bugs caused by drift between the copies:
    • For OpenAI-compatible embedders used through SemanticMemoryClient, base_url was not passed as openai_base_url, so the client connected to the official api.openai.com (the 0.5.24 fix existed only in the public convert_embedder_to_mem0, not on the path the client actually used).
    • The public convert_llm_to_mem0 lacked the gpt-5.x classification fix for Azure (normalizing the model key with mem0_classification_model()), so on Azure gpt-5.x deployments mem0 sent max_tokens and got HTTP 400 (the fix existed only in the instance method).
  • Fix: removed the async with example from the SemanticMemoryClient.cleanup() docstring — the async context manager is not implemented, so following the example raised TypeError.
  • Fix: _ensure_qdrant_collection now closes the QdrantClient it creates (fixes a connection leak on every client initialization).

0.5.35 (2026-08-25)

  • Feat: the Local Integration Lab is bundled in the package — with just pip install 'agenticstar-platform[lab]' and python -m agenticstar_platform.lab, PostgreSQL / Qdrant / S3-compatible storage (MinIO) start from a version-pinned Docker Compose file and a synthetic ingest → retrieve → artifact/result persist → terminal outcome journey completes without any credentials (doctor for read-only diagnostics, --reset to tear down). No cloud account, production credentials or .env editing. See Quick Start §2.5 and agenticstar_platform/lab/README.md.
  • Fix: QdrantConfig.check_compatibility was ignored on the non-auth path of QdrantManager — without an auth_token_provider the setting was not passed to QdrantClient, so the client/server version check (and its warning on a mismatch) always ran, even with check_compatibility=False.
  • Change: the qdrant-client floor in [rag] / [all] is raised to >=1.13.0 — qdrant-client 1.12 and earlier do not accept the check_compatibility argument (this also fixes an existing inconsistency where the auth path raised TypeError on 1.12 and earlier).

0.5.34 (2026-08-24)

  • Feat: MCP tokens with expires_at=null (no expiry) are accepted — personal access tokens (PAT) are long-lived or non-expiring, so the token supply API returns expires_at: null for them. Previously get_mcp_tokens() treated a missing expires_at as INVALID_TOKEN_DATA and discarded that provider's token, so PATs could not be used at all. MCPTokenInfo.expires_at is now Optional[datetime] = None (None = no expiry), and the client accepts null or a missing key as valid. Any other invalid value is now unified into a per-provider INVALID_EXPIRES_AT (an intentional error-code change: falsy values such as an empty string or 0 used to be INVALID_TOKEN_DATA, and truthy non-strings leaked into a broad except that turned the whole fetch into UNKNOWN_ERROR). If several of your components validate the same tokens in sequence, all of them need 0.5.34 or later before PATs pass.

0.5.33 (2026-08-21)

  • Fix: llm_usage_daily_pkey conflicts when rollup_llm_usage_daily() runs concurrently on several replicas — when replicas ran the hourly rollup in parallel, a later DELETE under READ COMMITTED could not see rows from an earlier run that had not committed yet, deleted 0 rows, and its INSERT then hit the primary key. The SQL function now starts with a pg_try_advisory_xact_lock(hashtext('rollup_llm_usage_daily'), p_day - date '2000-01-01') guard, and the side that cannot take the lock skips (the winner writes the same aggregate from the same ledger). The function is distributed through CREATE OR REPLACE in ensure_schema(), so the fix reaches the database only when a component that runs ensure_schema() starts on 0.5.33 (restarting an older component reverts to the old definition; after a mixed-version period, check with pg_get_functiondef).

0.5.32 (2026-08-12)

  • Added the guardrail alert writers (GuardrailAlertWriter / GuardrailAlertAggregates) — public producer writers for the admin triage view guardrail_alerts (a non-authoritative operational view; the action-audit ledger remains authoritative) and the unlinked daily aggregates guardrail_alert_daily_aggregates. Structurally enforced: rows have no end-user ID column, and SelfHarm is rejected at row construction with a synchronous ValueError (normalizing spelling variants and str-Enum .value) and recorded in the aggregates only. Rows are idempotent through event_key v2 (replays never roll back admin lifecycle state). Severities are measured per category (do not substitute thresholds). Grants: rows = INSERT only; aggregates = INSERT + UPDATE(count) + SELECT(count). Delivery follows the same bounded FAF + spill contract as the action audit.

0.5.31 (2026-08-10)

  • Added the action-audit module agenticstar_platform.audit (ActionAudit / GatewayActionAudit / ActionAuditWriteError / canonical_digest) — pure infrastructure that records action events (approvals, tool authorizations, policy violations, kill switch, external-agent calls) into append-only ledgers (agent_action_audit / llm_gateway_action_audit). Three write modes: record() = at-least-once while the process lives (bounded retry, then the sanitized row is spilled as JSON to a log line tagged action_audit_spill) / record_sync() = audit-before-act (raises ActionAuditWriteError when the DB write fails — an approval that cannot be audited must not take effect) / record_nowait() = fire-and-forget for hot paths (backlog capped at 512, pre-start cancellation also spills; best-effort, lost if the process dies abruptly). Usage contract: pass bodies as digests (canonical_digest); field caps (reason truncated to 300 chars / payload_digest must be 64 lowercase hex chars / metadata over 2KB replaced by a digest, always deep-copied) limit the blast radius of accidental leakage (there is no automatic secret detection or redaction). approval.* requires actor_ref (synchronous ValueError). No extra dependencies (stdlib only, part of core).

0.5.30 (2026-08-07)

  • Fix: run_marketplace_agent / arun_marketplace_agent always failed with AttributeError when data_access was omitted — the db_config / PostgreSQLConfig.from_env() fallback passed a raw PostgreSQLConfig to DataAccess and crashed with 'PostgreSQLConfig' object has no attribute 'initialize' before the agent ran (so the minimal run_marketplace_agent(my_agent) documented in 0.5.29 always failed). The config is now turned into a manager with create_postgresql_manager(config) before connecting. When api_url (DB_API_PROXY_URL) is set, the runner cannot supply a token_provider and raises MarketplaceRunnerConfigError explicitly. Regression tests were added for the fallback path.

0.5.29 (2026-08-01)

  • Added the Marketplace runner (run_marketplace_agent / arun_marketplace_agent) — an agent function that works locally can be handed straight to the Marketplace-compatible terminal lifecycle (identity validation → input fetch → run → result persist / webhook → exactly one terminal event → cleanup). Previously this main boilerplate was written by hand, and missing or duplicate terminal events depended on each developer. If an identity env var (EXECUTION_ID etc.) is missing, the runner stops with MarketplaceRunnerConfigError without calling the agent. pip install 'agenticstar-platform[runner]' (new extra, equivalent to db + webhook). The contract is pinned by contract tests (success / agent exception / input fetch failure / persist failure / webhook failure × exactly one terminal event).

0.5.26 (2026-07-27)

  • boto3 moved from [security] to [security-aws] — 0.5.25 added boto3 to [security] for AWSSecurityClient, which pulled boto3 back into lightweight installs that pin [storage-azure,security] and deliberately leave it out. To use the AWS content-safety backends, install pip install 'agenticstar-platform[security-aws]' (still included in [all]).

0.5.25 (2026-07-27)

Packaging: lightweight installs now actually work (previously only [all] did).

  • import agenticstar_platform succeeds on a core install — __init__.py imported every module unconditionally, so pip install agenticstar-platform (core) failed with ModuleNotFoundError: asyncpg and [db] failed with ModuleNotFoundError: openai at import time. It now uses PEP 562 lazy imports and works as the extras split in pyproject intends. Public names and import style are unchanged.
  • A missing extra produces an actionable error — instead of a raw ModuleNotFoundError, the error says which extra to install. Only cases where a dependency owned by that extra is actually missing are converted, so import bugs inside the SDK or circular imports are never misdiagnosed as a missing extra.
  • WebhookEventHandler / create_marketplace_handler detect a missing aiohttp when the symbol is accessed — previously the object could be created without aiohttp and silently skipped webhooks at runtime, leaving only a log line.
  • pydantic>=2.0.0 added to the core dependencies — the auth module uses it, but it was undeclared and [all] only worked by accident through transitive dependencies.
  • boto3 added to [security] — required by AWSSecurityClient (Comprehend / Bedrock Guardrails) but undeclared.
  • [all] includes the [security] dependencies (google-cloud-dlp / google-cloud-aiplatform) — previously GCPSecurityClient could not be used even with [all].
  • The README Quick Start is runnable — previously it only defined async def main() without calling it, so copying it did nothing. It was replaced with a minimal example that runs from progress to terminal outcome without external services, plus a drift test that extracts and runs the code from the README.

0.5.24 (2026-07-21)

RAG/Memory: OpenAI-compatible embedding endpoints (embed-v-4-0 class) support.

  • RAG: EmbeddingConfig gains a provider field ("azure" default / "openai") — provider = "openai" targets OpenAI-compatible endpoints such as embed-v-4-0 on Azure AI inference (base_url must include /models; api_version is not used). Model strings with a LiteLLM-style prefix (azure/... / openai/...) derive the provider automatically and the prefix is stripped from the deployment/model name.
  • Memory: convert_embedder_to_mem0() passes openai_base_url for the openai provider — when [memory.embedder] uses an openai/... model with base_url set, the Mem0 embedder now connects to that endpoint. ⚠️ Without this release, the base_url was silently dropped and the client connected to api.openai.com, causing 401s with non-OpenAI keys.
  • RAG: embedding inputs are truncated with a model-aware token budget — 8,000 tokens for 8k-class models, a 100k sanity cap for long-context embed-v* models; disallowed_special=() so special-token literals (e.g. <|endoftext|>) in documents cannot crash encoding; character-based fallback when tiktoken is unavailable.

0.5.23 (2026-07-09)

Security: Azure PII detection batching and quota-aware 429 retry.

  • Security: detect_pii_batch() — Azure PII calls are batched at 5 documents/request — all sliding windows across the input texts are packed into a single documents array (Azure sync PII allows 5 docs/request), cutting Azure call volume by up to 5×. Long conversation histories previously issued one Azure call per text element, so a single long request could exhaust the S0 limit of 300 requests/min. SecurityClientBase gains a sequential default implementation, so AWS / GCP clients inherit the API unchanged. detect_pii() is now a single-text wrapper over the batch path — external behavior (fail-closed empty string, error codes, signature) is unchanged.
  • Security: Azure PII 429s are retried honoring Retry-After, then abort with RATE_LIMITED — throttled requests are retried (default 2 retries, AGENTICSTAR_AZURE_LANG_429_RETRIES, delay capped at 5s). If throttling persists, the remaining batch chunks are aborted (no further quota pressure) and unresolved texts fail with error_code="RATE_LIMITED", letting callers surface 429 + Retry-After to their clients instead of a retry-inducing 502. Previously any non-200 (including 429) failed closed immediately with no retry.
  • Security: fail-closed hardening for partial batch responses — a 200 response missing a submitted document (absent from both documents and errors) now fails that text closed instead of silently passing it through unscanned.

0.5.22 (2026-07-08)

Security (GCP Content Safety), RAG error hierarchy, and DB identifier validation.

  • Security: GCP Content Safety gains a Vertex AI Safety Filters path (+ Gemini judge) — GCPSecurityClient can now moderate content via Vertex AI safety filters across all Marketplace regions, with a Gemini-based semantic judge as an additional layer. Adds google-cloud-aiplatform>=1.60.0 to the [security] extra.
  • Security: guardrail input-inspection window count is now env-configurable — the number of sliding windows scanned on large inputs is tunable, relaxing the earlier "large-input tail not inspected" gap.
  • RAG: QdrantConfigError folded into the VectorStoreError hierarchy; initialize() is idempotent — init-failure wrapping now passes already-typed exceptions through (config errors are no longer mislabeled), and calling initialize() on an existing collection re-ensures the payload indexes instead of raising.
  • DB: identifier validation consolidated into common.validation; errors returned as a uniform dict — select_one returning None on error is now documented, and identifier-validation failures return a consistent shape.

0.5.21 (2026-06-25)

Metering: persist missing_reason on the ledger.

  • UsageMeter.record(..., missing_reason=...) + new missing_reason column on llm_usage_ledger — the usage-missing reason (provider_omitted / stream_interrupted / …) is now stored on the row (previously logs only), so offline exports can distinguish cost_usd IS NULL (unpriced) vs usage_status='missing' vs true-zero. Present rows store NULL (no contradiction). ensure_schema() adds the column idempotently (ADD COLUMN IF NOT EXISTS) — backward-compatible, propagates to existing monthly partitions; no change to existing columns.

0.5.20 (2026-06-24)

Metering: billing-clean cost storage + cache-read fallback.

  • round_cost_usd helper + UsageMeter.record stores cost as a rounded Decimal — avoids float-repr drift (0.00089999…) in the cost_usd numeric column; 8-dp rounding does not change billing sums, and NaN/Inf are stored as NULL. Exported from both agenticstar_platform and agenticstar_platform.metering.
  • extract_usage reads top-level cache_read_input_tokens as an additional cached_tokens fallback (complements the 0.5.19 details-based extraction), improving cache-aware cost on the token-only path. No public API change.

0.5.19 (2026-06-20)

Metering cost-accuracy fixes (cache-aware). Supersedes 0.5.18.

  • default_cost_fn fallback applies prompt-cache pricing — passes cache_read_input_tokens / cache_creation_input_tokens to litellm.cost_per_token when completion_cost(response) is unavailable, so cache-heavy calls are no longer priced at the full input rate.
  • extract_usage covers Responses API + cache-creation — reads cached_tokens from input_tokens_details (Responses) as well as prompt_tokens_details (Chat), and propagates cache_creation_input_tokens (Anthropic). Previously cached tokens on Responses calls were missed on the token-only/fallback path. No public API change.

0.5.17 (2026-06-19)

  • Security: AWS PII detection now defaults to Bedrock Guardrails (multilingual) — AWSSecurityConfig gains a pii_service field ("bedrock_guardrails" default / "comprehend" legacy). AWS detect_pii() routes through Bedrock Guardrails sensitiveInformationPolicy by default, fixing multilingual (including Japanese) PII masking — Amazon Comprehend DetectPiiEntities only supports en / es (previously ja etc. slipped through and raised a ValidationException). ⚠️ Behavior change: with the new default, AWS PII requires guardrail_id to be set (otherwise it returns NOT_CONFIGURED); set pii_service="comprehend" to keep the legacy en/es path. Configs that already set pii_service explicitly are unaffected.

0.5.16 (2026-06-14)

Marketplace SDK reliability fixes (surfaced while building agents from the guides alone):

  • Config: from_toml() resolves ${ENV} placeholders and uses the stdlib tomllib (no external toml dependency). host = "${POSTGRESQL_HOST}"-style values in config.toml are expanded from the environment; unresolved placeholders are left intact. Supports ${VAR} and ${VAR:-default}.
  • Memory: mem0 2.x compatibility — SemanticMemoryClient.search() / get_all() now use mem0 2.x filters / top_k internally (the public user_id / limit arguments are unchanged). Azure gpt-5.x / o-series memory models no longer fail with max_tokens is not supported (the unsupported parameter is suppressed; the real Azure deployment is still targeted). mem0ai is pinned to >=2.0.0,<3.0.0.
  • Events: EventEmitter.drain() — drives registered handlers (e.g. the marketplace webhook handler) for non-SSE flows. emit_event(...) only enqueues; without an SSE consume_events() loop, call drain() so handlers actually fire (previously emit + cleanup silently dropped events). The [webhook] extra now includes aiohttp (required by WebhookEventHandler); [all] includes it too.
  • Storage: StoragePaths.input_uploads_prefix(user_id, conversation_id, message_id) — builds the owner-scoped prefix (users/{user_id}/uploads/{conv}/{msg}) for input attachments uploaded from the chat UI. Use with download_objects_by_prefix.
  • Runtime: wait_for_egress() — waits for the egress sidecar to accept connections before the first outbound call, avoiding a startup race that could skip first-turn input moderation / PII.
  • PodRuntime: injectable self scale-down — PodRuntime(..., scale_down_callback=...); when omitted and no bundled scaler is present, scale-down is skipped cleanly instead of raising ModuleNotFoundError.

0.5.15 (2026-06-13)

  • New: Metering module (UsageMeter) — dedicated LLM usage & cost ledger. meter.record(...) computes cost from a response/usage and writes one row per call to llm_usage_ledger; rollup_recent() / daily_cost(...) provide daily aggregation and cost-visualization queries; ensure_schema() creates the (portable) tables/rollup function idempotently. Cost is pluggable — default default_cost_fn uses litellm if installed (long-context / cache / tier aware), gracefully records tokens-only (cost_usd = NULL) when litellm is absent, and cost_fn= overrides. Agent-logic agnostic; identical for self-hosted and Marketplace BYO. Exports: UsageMeter, default_cost_fn, extract_usage.

0.5.7 (2026-05-10)

  • Security: PII confidence threshold per-call — detect_pii() now accepts an optional confidence_threshold parameter on Azure / AWS / GCP clients (and the SecurityClientProtocol / SecurityClientBase). Passing None falls back to the value in *SecurityConfig.pii_confidence_threshold. This lets a single long-lived SecurityClient instance serve callers that need different thresholds, instead of constructing a new client per request. Backward compatible — existing callers that omit the new argument get the previous behavior.
  • Security: GCP threshold now respects config — GCPSecurityClient.detect_pii() previously hardcoded a LIKELY (likelihood ≥ 4) cutoff and ignored GCPSecurityConfig.pii_confidence_threshold. It now compares likelihood / 5.0 against the configured threshold, matching Azure / AWS behavior. With the default pii_confidence_threshold = 0.7 the effective cutoff stays at likelihood ≥ 4, so most callers see no change. Callers that had set pii_confidence_threshold below 0.7 will start seeing additional POSSIBLE (likelihood 3) findings.
  • Reuse the client to avoid leaks — AzureSecurityClient (and AWS/GCP equivalents) hold an httpx.AsyncClient (TLS context + connection pool) internally. Construct one client per process and call await client.close() on shutdown (or use async with); creating a new client per request without closing leaks resources.

0.5.2 (2026-03-28)

  • Memory: Removed episodic memory (Graphiti/FalkorDB) — episodic.py was unused dead code. SDK now provides semantic memory (Mem0) only.
  • Extras: [semantic] / [episodic] replaced with [memory] — unified extra for Mem0-based semantic memory.
  • Extras: [all] no longer includes graphiti-core[falkordb].
  • README updated to reflect episodic memory removal.

0.5.0 (2026-03-25)

Breaking Changes:

  • DB: DataAccess now takes a manager instance instead of (config, use_proxy, token_provider). Callers create PostgreSQLManager or ApiPostgreSQLManager and pass it directly.
  • DB: use_proxy parameter removed from DataAccess, create_postgresql_manager().
  • DB: api_proxy_url renamed to api_url in PostgreSQLConfig.
  • DB: ApiPostgreSQLManager exported as public API for HTTP API access.

Improvements:

  • DB: is_initialized() method added to both PostgreSQLManager and ApiPostgreSQLManager.
  • Qdrant: prefer_grpc / check_compatibility are now explicit QdrantConfig fields (no longer hardcoded based on auth_token_provider).
  • Error messages no longer reference deployment-specific terms.

0.4.0 (2026-03-21)

  • Security: Prompt Shield documents trimming - check_prompt_shield() now trims each document to 10,000 characters to comply with Azure API limits. Previously, WebFetch results exceeding 10,000 characters were blocked even without violations.
  • Security: PII detection language support - detect_pii() now accepts a language parameter (default: "ja") for accurate multi-language PII detection. Previously hardcoded to Japanese.
  • Security: Protocol/ABC updated - SecurityClientProtocol and SecurityClientBase updated with language parameter in detect_pii().

0.3.2

  • Storage module: Multi-cloud support (Azure Blob, S3, GCS)
  • Auth module: AgenticStar Auth API client
  • Memory module: Semantic memory (Mem0)

Version

3.1.3

Metadata

Release files for agenticstar-platform 3.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agenticstar-platform 3.1.3
File Size Uploaded Provenance
agenticstar_platform-3.1.3.tar.gz 1.0 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agenticstar-platform 3.1.3
File Interpreter ABI Platform
agenticstar_platform-3.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / agenticstar_platform-3.1.3.tar.gz

Download URL agenticstar_platform-3.1.3.tar.gz
Size 1.0 MB
Tags Source
SHA-256 checksum
How to use checksums
66e2da574278ae9b7641b7e7a07c81c29373aa0dedce5b26e265325d4853f961
BLAKE2b-256 checksum
How to use checksums
5cbfa5400793dd186497844eb97e93b9cd5ce6da24dbaf3237527cb632d0eb92
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / agenticstar_platform-3.1.3-py3-none-any.whl

Download URL agenticstar_platform-3.1.3-py3-none-any.whl
Size 372.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
915fdd630c786c7ca10bc167b54ec3bf4dc2e27bde6d941d784b6719dc89825d
BLAKE2b-256 checksum
How to use checksums
2624a8ef7ae2ee0c6ee3803dbe825fae474d8028e0650facdcb843dab62221c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for agenticstar-platform 3.1.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

3.1.3 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page