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
MarketplaceRunnerConfigErrorwithout 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())
outcomeis one ofcompleted/incomplete/failed/timeout/stopped/denied/skipped. Anything else raisesValueErrorreasonis 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 insummary- The platform uses only
outcomeandreasonfor the execution's result.had_mutating_successandsummaryare 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.Falseafter 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_agentexits 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
})
Recommended convention fields
| 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:
intis stored as-is; alist/tupleis normalized to its length.- Not measured is
NULL; measured-and-zero is0— the two are kept distinct. - Values the column cannot hold (a breakdown list, a wrong type, or a value outside the PostgreSQL
integerrange) are additionally kept undermetadata.tools_used, so nothing is lost. The column value itself never fails a telemetry write; a raw value kept inmetadatafollows the same JSON-serializability rule as any other metadata field.
tools_usedbecame a known column in 0.5.38 (use 0.5.39 or later). Before that it fell through tometadata; if you readmetadata->>'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; gracefulNULLwhen litellm is absent).TelemetryAccessremains the place for agent operational telemetry (intent / tools / duration →ai_telemetry);UsageMeteris 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). Passcost_fn=...to override. - DB: any handle exposing
execute_query(query, params) -> {success, data, error}(the SDK'sDataAccess/PostgreSQLManager). - Usage status (SDK ≥ 0.5.21): each row records
usage_status(present/missing); when usage is absent, passmissing_reason=(e.g.provider_omitted/stream_interrupted). It is stored on the row (not just logs) so offline exports can tellcost_usd IS NULL(unpriced) vsusage_status='missing'vs true-zero apart. Present rows always storeNULL(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_tokensis 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) intopayload_digest. The SDK performs no automatic secret detection or redaction. Field caps (reasontruncated to 300 chars,payload_digestmust be 64 lowercase-hex chars or it is dropped,metadataover 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 inmetadataother 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 taggedaction_audit_spillfor manual reconcile) /record_sync()= audit-before-act (raisesActionAuditWriteErroron 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_releasedandworkitem.*raiseValueErrorsynchronously without it (SDK ≥ 1.0.0a1; before that onlyapproval.*).
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) raisesValueErrorsynchronously; SelfHarm-only detections are recorded as aggregates only. Rowpolicy_outcomeis alwaysblocked. - 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'sDataAccess/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 unremovablefloorof forbidden patterns, whether rationale bodies may be retained, version) and a project policy (ProjectPolicy: profile,deny/allowpatterns, gate settings).tighten_merge(l1, project)combines them with two guarantees: the tenantfloorcan never be removed, and the project profile can never be looser than the tenant default. Astrictproject'sallowlist permits the listed write / destructive tools only within what the tenant L1 profile permits (since 1.0.0a3l1_profile: it never re-enables an operation class the L1 profile forbids, e.g. destructive under aguardedtenant) — put anything that must stay forbidden regardless of profile into the tenantfloor. - 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 projectstrictallowlist never re-enables an operation class the L1 profile forbids, e.g. destructive under L1guarded; 1.0.0a3l1_profile). evaluate(policy, tool, op_class, ...)returns aDecision(mode, reason, policy_version, unregistered, matched, layer). For a forbid,layertells 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) orsystem(not a rule: no policy, or the arguments could not be evaluated). It isNonefor an allow. The caller classifies the tool (op_class=read/write/destructive, orNonefor unknown); for built-in tools,dry_run=Trueforbids everything but reads;mcp_server="x"matches bothmcp:x:<tool>and<tool>.- MCP and A2A calls (SDK ≥ 3.0.3 / 3.0.4): pass
mcp=Truewith the connection'sconnection_id, orconnection_kind="a2a"with the A2A connection'sconnection_id. These calls are not classified by operation class and are allowed (reasonmcp_unrestricted/a2a_unrestricted) unless theflooror an explicit forbid rule matches — anexternalrule for that connection ID, or an unscoped tool-name glob such asmcp:<server>:<tool>. An MCP call withoutconnection_idwhile external MCP rules exist, or an A2A call withoutconnection_idwhile A2A rules exist, cannot be identified and is forbidden. - Snapshots:
EffectivePolicy.to_dict()/from_dict()round-trip anddigest(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's503while 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 (SecurityAPIErrorwitherror_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
CONNECTwith503. A workflow agent whose first external call is the input guardrail failed withSecurityAPIErroron 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=1still 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 getNone, and so does a value of the wrong shape (kindnot a string orexternalnot a boolean;subject/textthat are not strings becomeNone). - It is not added to
context.manifest(the start record saved to the audit). - Platform requirement: the platform runtime API returns
start_inputin the context (Marketplace 3.2). Against a platform that does not,start_inputis alwaysNoneand 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.dimensionsstaysNone, 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": 1024infrom_dict), it is sent as the API'sdimensions(except when the model or deployment name ends withtext-embedding-ada-002, which rejects it; an ada-002 deployment under another name must not setdimensions) and every returned vector must have exactly that length; otherwiseEmbeddingDimensionError(a subclass ofEmbeddingError) 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 andtext-embedding-3-*support shorter vectors). A value that is not a positive integer (a string of ASCII digits is accepted;true,1.5or"²"are not) raisesValueErrorwhen 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=1536explicitly — including configs copied from the earlier[rag.embedding]example in this README, which setdimensions = 1536— now sendsdimensions=1536to the API and checks the returned length. Models that support shorter vectors accept it; for a model or gateway that rejects the parameter, leavedimensionsout. QdrantConfig.vector_sizemust match the generator's dimension, as before (QdrantManagerraisesQdrantConfigErrorotherwise). Passvector_size=generator.dimensions.vector_size=Nonenow means 1536, soQdrantConfig(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"infrom_dict) is passed toconvert_embedder_to_mem0asembedding_dims, and should matchembedding_model_dimsof the vector store. What happens next is Mem0's own behaviour, which the SDK does not change: the OpenAI-compatible embedder sends it asdimensions, Gemini asoutput_dimensionality, Bedrock only for Titan v2 models (Cohere and Titan v1 ignore it), the Azure embedder does not send it (senddimensionsyourself there); the returned length is not checked, andtext-embedding-ada-002rejects the parameter (do not setdimensionsfor it). - New helpers:
parse_embedding_dimensions(value)(exported fromagenticstar_platform.rag) andDEFAULT_EMBEDDING_DIMENSIONS(1536). - Dependencies: the
rag,labandallextras now requireopenai>=1.10.0(the first release whose embeddings API acceptsdimensions). - PII detection languages:
AzureSecurityClient.detect_pii/detect_pii_batchmap 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/executeproxy used in CLI and Pod mode) resends the proxy's "not processed" answers. The proxy answers503before 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 leastRetry-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'sRetry-After: 1, at most about 40 s). Any other503or5xx, including one from something else on the way, may arrive after the query ran and is still returned asAPI_ERRORwithout a retry; so is a resendable answer on the last attempt.- Idle connections to the proxy expire after 4 s (
httpxkeep-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_provideronApiPostgreSQLManager(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 (astrorint). 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. AzureBlobStorageClientcan now sign in without a connection string.AzureBlobConfig.connection_stringis 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 newstorage_account_name(plusendpoint_suffix, defaultcore.windows.net) and authenticates withDefaultAzureCredential(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 raisesStorageConfigErrorat 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.0is added to thestorage-azureandstorageextras. It is imported only on the credential-less path, soAzureBlobStorageClientstill resolves withazure-storage-blobalone.
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/executeproxy used in CLI and Pod mode) now sendsparam_typesnext toparams: one entry per parameter,"datetime","date"orNone, 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 plainstrsuch as"2026-10-09"or"2026-10-09T01:02:03"bound to a TEXT column arrived asdate/datetimeand asyncpg rejected the statement (expected str, got date). Realdate/datetimevalues, including tz-aware ones, keep arriving as before.- Compatibility: a proxy that does not know
param_typesignores it and keeps guessing as before; a new proxy keeps guessing for an SDK that does not send it. The directPostgreSQLManageris 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 fromagenticstar_platform.db; requires the[db]extra). A cycle execution (execution_data.runtime_protocol=mp-chat/1) that exits 0 was always recorded asoutcomeNULL /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_successandsummaryare 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 returnsFalseinstead of raising, so the agent's own work is never failed by reporting. Only an outcome outside the 7 values raisesValueError; 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 byCONVERSATION_ID/MESSAGE_IDlike 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 receivedASTER_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/procis 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()/WorkflowRunnerdo not change the process unless you passprotect_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_closedanddeadline_exceededkeep exit code 2 so the platform closes the run without waiting for the collection deadline. context.logreaches the pod log. The SDK configured no log output, so with the recommended start commandcontext.log()and the runner's INFO messages were dropped.run()adds a stderr handler to theagenticstar_platformlogger 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/ finitefloat/boolare 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-stringerror_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 ofexit_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_agenttake a started fence before calling the agent. A cycle execution is one whoseexecution_data.runtime_protocolismp-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 takespg_advisory_xact_lockon the execution's (CONVERSATION_ID,MESSAGE_ID), checksexecution_messagesfor any row of that pair, and writes one marker row (chunk_typeprogress_message,content_data.kindsdk.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 newMarketplaceRunnerFenceTaken; 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_accessthat has no pool it behaves as before. - New
started_fenceargument (defaultTrue). The fence uses the direct PostgreSQL pool (PostgreSQLManager.pool). For a cycle execution with adata_accessthat has no pool (HTTP-API mode or your own implementation), the runner raisesMarketplaceRunnerConfigErrorunless you passstarted_fence=False; then a recreated Pod may run the agent again. - Admin dashboards that aggregate
execution_messagescount 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 isproject,tenant,floororsystem(constantsLAYER_PROJECT/LAYER_TENANT/LAYER_FLOOR/LAYER_SYSTEM); for an allow it isNone. A forbid isprojectonly 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 (reasonandmatchedstay 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 byprofileistenantwhen the tenant's default profile isguarded, andprojectwhen the project choseguardeditself. Up to 3.0.10 the reason code alone could not tell these apart. mode,reasonandmatcheddo not change. Code that compares wholeDecisionobjects with==must includelayer(a forbid without a policy is nowDecision("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_sha256argument onAgenticStarAuthClient.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_backorproceed.decided_by:human, orsystemwhen 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.Nonewhen the reviewer wrote nothing or the decision was automatic.decided_at: when the decision was made (ISO 8601).
It is
Nonefor 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; usenoteto tell your agent what to fix. -
context.manifest(the start record saved to the audit) includesgate_decisionwithgate_id/decision/decided_bywhen the run follows a review. The reason / comment text is not included. -
The sample
docs/examples/workflow_reply_agent.pyadds the reason to its instructions after a send-back. -
Platform requirement: the platform runtime API returns
gate_decisionin the context. Against a platform that does not,gate_decisionis alwaysNoneand 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 asuser_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 ofuser_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, asuser_promptalready 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'sdocker-compose.yml, and the auditROW_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 twoValueErrormessages raised byGuardrailAlertWriterno 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_reasonto 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 onis_cancel_requested()returnsTruewithout IPC, andensure_can_act()/connection_token()raiseWorkflowError. 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/ValueErroron a closed pipe) now raisesWorkflowError(runner closed the control channel). An agent that catchesWorkflowErrorno 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_ttlseconds 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_atis null) are refreshed everyUNKNOWN_EXPIRY_REFRESH_SECONDS(600 seconds). - Raises
WorkflowErrorfor: 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,
WorkflowErroris raised. A cached value that dropped belowmin_ttlwhile 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_tokenand 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 opconnection_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 callconnection_token, only the two changes below apply).
- The cached value is returned until less than
WorkflowContext.is_cancel_requested()/ensure_can_act(): a stop that arrives while the caller is cancelled mid-check is still recorded (subsequent calls returnTrue). The runner's cancellation check (IPC opcheck) 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_exceededwhile input artifacts are being staged, the runner maps it to a stop reason and sends a completion instead of crashing (stopped/cancelled; a deadline becomestimeout/execution_timeout). The agent is not started (had_mutating_successis 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 asstopped/cancelled). Other API errors (unreachable, 5xx after retries, unexpected 4xx) still raise as before. AgenticStarAuthClient.get_user(user_id):user_idis URL-encoded as a single path segment (values containing/or?can no longer reach a different API path)..and..are rejected withINVALID_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 emitconnection_kindfor mcp). Ana2arule 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 isallow(reasona2a_unrestricted). If a2a rules exist but the connection cannot be identified (connection_idis 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_version4 (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 asmcp(3.0.3 callers are unchanged). It raisesValueErrorif a2a rules exist but there is noa2amap, 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)returnsallow(reasonmcp_unrestricted) unless the call matches the floor or an explicit forbid rule (an externalconnection_id+ method glob, or an unscopedmcp:<name>:<tool>glob). The profile operation-class table (open / guarded / strict),dry_run,delegatedand the unclassified (unknown_tool) stage do not apply to MCP. Anop_classpassed by the caller is ignored (floor entries conditioned on op_class do not apply to MCP).unregisteredis not set (having no classification is normal).- Unchanged: no policy = forbid; the order of floor / forbid-rule matching; an MCP call without
connection_idwhile 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_onlynow pass. Put MCP methods you want to block into an external forbid rule (connection ID + glob) or the floor. Audit reason codes gainmcp_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'sconnection_id.mcp_serverremains the connection's display name (used for themcp:<name>:<tool>candidates in allow / unscoped forbid globs, never for identification). For an MCP call (mcp_serverset) withconnection_idNone, the connection cannot be identified, so if any external rule exists the call is unevaluable (forbid withunevaluable_args; the name is never used as a substitute). In-process MCP servers that are not registered connections passLOCAL_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 (withmcp=True, a call with an empty or blank name and no ID is also an unidentifiable MCP call = forbid; only legacy callers that omitmcpkeep 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 raisesValueErrorotherwise. 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 containserverare 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).serveron aninternalrule still raisesValueError.- 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/degradedreplaced bycompleted/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 iscompleted(reason codecompleted). The olddegraded/effects_unobserved/effects_partial_failure/no_observation/failed/effect_failedno 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'shad_mutating_successare nowOptional[bool]. Audit persistence failures, writes with no effect or with an unknown status, a runner restart / stop / incomplete cleanup, and termination elsewhere yieldNone(unknown) unless a success was observed. The platform does not automatically retry unsuccessful runs withNone, 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_versionare 2. Thegate_requested(always null) andverificationfields are removed; how the runner itself ended (stop reason / restart / incomplete cleanup) is carried inrunner(the argument is renamed fromverificationtorunner).decide_outcome()gainseffects_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), becauseskipped/policy_unavailablemakes the platform re-check the policy and run again. The platform routes this reason code to a human as effect-unknown. - Breaking:
countskeys renamed:tools_invoked→tool_calls,reads→read_calls(the same keys the platform's execution records and exports read).mutating_success/mutating_failedare unchanged, and so are the SDK-only observations (mutating_invoked/mutating_unresolved/orphan_effects/effect_conflicts/denied/audit_persist_failures). Consumers that readcounts.tools_invoked/counts.readsfrom stored envelopes must be updated. workflowexportsENVELOPE_VERSION(= 2).verification.completedremoved fromaudit.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 after1.0.3. The number was chosen to align with the product, not as a semver major bump. It does, however, contain changes incompatible with1.0.3: the three removals from the unreleased1.0.4(see 1.0.4 below:items_advanced/no_items/claim.stale) are included. Check that entry when upgrading from1.0.x. llm_usage_ledgergainscache_read_tokens/cache_creation_tokens(cached_tokensis 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 tablellm_usage_dailyand its function are unchanged (aggregate the breakdown from the ledger by date).extract_usage()returns the breakdown ascache_read_tokens/cache_creation_tokens. Explicit values (including 0) take precedence; otherwise the previously resolvedcached_tokens(by convention: reads; including provider details /cache_read_input_tokens) andcache_creation_input_tokensare 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 indefault_cost_fn()returns None).- Callers that put reads + writes combined into
cached_tokensshould pass the breakdown explicitly. Thedefault_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 throughLEDGER_MIGRATIONS(idempotent). Environments where a migration already added the columns do not needensure_schema(). If you use neither, apply the sameALTER TABLE … ADD COLUMN IF NOT EXISTSbefore upgrading to 3.0.0 (as whenmissing_reasonwas added). If your DB manager reports failures as a return value (success=False), neitherensure_schema()norrecord()turns that into an exception (as before; with the defaultfail_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_advancedremoved from the output ofworkflow.build_envelope()(other envelope keys are unchanged).no_itemsremoved fromworkflow.envelope.SKIP_REASONS(context.skip("no_items")raisesValueError; for a step with nothing to process useno_target/not_applicable/nothing_to_do).claim.staleremoved from the audit vocabulary.KNOWN_EVENT_TYPESis 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 writesdescription_ja/description_enin terms of operations and no longer names internal tools. Kind IDs, labels,mapping_version, the full internal mapping (internal_operation_mapping()/kind_tools()) andcommand_execcovering 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 withbind_connections(policy, {id: name}); removed in 3.0.2 — rules are now matched by ID throughevaluate(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_VERSIONSare public.EffectivePolicy.from_dict()in 1.0.1 and earlier rejects schema 3 withValueError(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(), andagenticstar_platform.policy.INTERNAL_OPERATIONS_MAPPING_VERSION(MAPPING_VERSIONinside the module). evaluate: surrounding whitespace is stripped frommcp_serverand an empty string is normalized to None. Apart from that normalization, matching of legacy bare / glob rules is unchanged; legacy digests andretain_rationale_bodyare 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 samecompletion_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:
startedis 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/WorkflowRunneralone do not). A 2xx runtime API response whose body is not JSON is normalized toRuntimeApiError(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.0to 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, andensure_schemasupport for them) with the 0.5.41 fix to result-set detection inPostgreSQLManager.execute_query()(rows fromWITH … SELECT/VALUES/TABLEwere 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
EffectivePolicygainsl1_profile(the tenant L1 default profile) andschema_versionis raised to 2.to_dict()includesl1_profile;from_dict()requires it and rejects stored forms whoseprofileis looser thanl1_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 L1guardedtenant (destructive forbidden), astrictproject with an allow entry still getsforbid(reasonprofile) for destructive. The permitted set with a project applied is a subset of L1's own permitted set (an L1strictallowlist 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=...):sourceis "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 onsource_event_id,project_id,(root_execution_id, occurred_at, id)). Tables created by older SDKs get the columns throughACTION_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(knownevent_typenames and their meanings; descriptive, not enforced). ExportsACTION_AUDIT_DDL/ACTION_AUDIT_INDEXES/ACTION_AUDIT_UPGRADE_DDL. - The
agenticstar_platform.policydocumentation was rewritten in product terms, and the guarantees oftighten_mergeare 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_auditcolumns:actor_kind(closed vocabularyagent / human / system / human_external / unknown; mapped fromactor_typewhen omitted),project_id(UUID),root_execution_id(text) andsource_event_id(deduplication key,^[A-Za-z0-9_.:@+-]{1,160}$). Existing calls that pass onlyactor_typekeep working. - Agent rows (
actor_kind='agent') withoutactor_refget the value ofActionAudit(agent_ref=...)(defaultagent:<source>). Human / system decision events (approval.*plusgate.resolved/content.deleted/forensic.*/policy.changed/item.quarantine_released/workitem.*) requireactor_ref(ValueErrorotherwise). - Older schemas: on a database without the new columns, the writer detects
UndefinedColumnonce 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 formetadata(project_id / run_id / step_id / item_id / tool_call_id / root_execution_id).- Numbers in
metadataother 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 inmetadatano longer fail the write (the metadata is replaced with{"unserializable": true}and the row is kept). - The
row_hashcanonicalization is specified inROW_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).
- Writers fill the new
- 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) andevaluate()(precedence: policy unavailable → forbid / floor or explicit forbid / unevaluable / unknown tool (open and guarded allow withunregistered; 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.5while a final release exists, so existing installs did not pick up an alpha automatically; installing one required an exact pin (==1.0.0a1, no--preneeded). 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 ofWITH ... SELECT/VALUES/TABLE— whether a statement returns a result set was decided from the query text (starts withSELECT/ containsRETURNING), so row-returning statements that did not start withSELECTwent throughconn.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 returnList[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 withSELECT, noRETURNING) still executes and returns a status dict; other multi-statement queries (SELECT 1; SELECT 2, or with parameters) still fail withDB_QUERY_ERRORas 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 previousconn.fetch()on asyncpg 0.29/0.30 (no named statements are left behind, even with PgBouncer transaction pooling).command_timeoutis 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 containedRETURNING; it now returns a status dict because it has no result set. Misdetection caused by column names or comments containingRETURNINGis gone. DataAccess.execute_query()delegates to this method, as does the platform's HTTP DB API thatApiPostgreSQLManagercalls, so both are fixed as well.
- Backward compatibility: statements without a result set (INSERT/UPDATE/DELETE/DDL) still return
0.5.40 (2026-09-01)
- Guardrail alerts: finer classification
SOURCE_SURFACESgainsagent_worker(for producers on the agent runtime surface), accepted by both the row writer and the aggregates writer.guardrail_alertsgains apolicy_categorycolumn (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).GuardrailAlertWriterrejects values outside the vocabulary synchronously withValueError(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 (theensure_schemaDDL applies only when the table is created).- The recursive SelfHarm guard now also scans
prompt_shield_origin/policy_category.
- Security:
PromptShieldResultgainsuser_prompt_attack/documents_attack— the breakdown by attack path, direct (userPrompt) / indirect (documents). Previously these were OR-ed across windows, leaving no way to fillguardrail_alerts.prompt_shield_origin. Both are optional (None= not available, distinct fromFalse= evaluated and not detected).attack_detectedis unchanged (backward compatible).
0.5.39 (2026-08-27)
- Fix: the metadata fallback check for
tools_usedcompared 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 existingmetadata['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_usedcolumn itself works in 0.5.38).
0.5.38 (2026-08-27)
- Fix:
ai_telemetry.tools_usedfell through to metadata instead of its named column —TelemetryAccess.save_telemetry()did not listtools_usedinknown_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 oflist_telemetry().- Type contract: accepts
int/list/tuple; a list or tuple is normalized to its length (the column is anintwhether callers pass a count or a list). - Not measured is
NULL; measured and zero is0.bool, negative values, values outside the PostgreSQLintegerrange and uninterpretable types leave the columnNULL(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, notnull(nullmeans not measured). - Added the list of known columns (reserved field names) to the Telemetry section of this README.
- Type contract: accepts
0.5.37 (2026-08-27)
- Fix: the OpenAI-compatible LLM conversion keeps an explicit
base_urlas Mem0'sopenai_base_url— the openai branch ofconvert_llm_to_mem0()droppedbase_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). Withoutbase_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
memorywas unified into module functions —SemanticMemoryClient's_normalize_provider/_parse_model_string/_get_api_key/_convert_llm_to_mem0/_convert_embedder_to_mem0now 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_urlwas not passed asopenai_base_url, so the client connected to the official api.openai.com (the 0.5.24 fix existed only in the publicconvert_embedder_to_mem0, not on the path the client actually used). - The public
convert_llm_to_mem0lacked the gpt-5.x classification fix for Azure (normalizing themodelkey withmem0_classification_model()), so on Azure gpt-5.x deployments mem0 sentmax_tokensand got HTTP 400 (the fix existed only in the instance method).
- For OpenAI-compatible embedders used through
- Fix: removed the
async withexample from theSemanticMemoryClient.cleanup()docstring — the async context manager is not implemented, so following the example raisedTypeError. - Fix:
_ensure_qdrant_collectionnow closes theQdrantClientit 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]'andpython -m agenticstar_platform.lab, PostgreSQL / Qdrant / S3-compatible storage (MinIO) start from a version-pinned Docker Compose file and a syntheticingest → retrieve → artifact/result persist → terminal outcomejourney completes without any credentials (doctorfor read-only diagnostics,--resetto tear down). No cloud account, production credentials or.envediting. See Quick Start §2.5 andagenticstar_platform/lab/README.md. - Fix:
QdrantConfig.check_compatibilitywas ignored on the non-auth path ofQdrantManager— without an auth_token_provider the setting was not passed toQdrantClient, so the client/server version check (and its warning on a mismatch) always ran, even withcheck_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 thecheck_compatibilityargument (this also fixes an existing inconsistency where the auth path raisedTypeErroron 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 returnsexpires_at: nullfor them. Previouslyget_mcp_tokens()treated a missing expires_at asINVALID_TOKEN_DATAand discarded that provider's token, so PATs could not be used at all.MCPTokenInfo.expires_atis nowOptional[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-providerINVALID_EXPIRES_AT(an intentional error-code change: falsy values such as an empty string or0used to beINVALID_TOKEN_DATA, and truthy non-strings leaked into a broad except that turned the whole fetch intoUNKNOWN_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_pkeyconflicts whenrollup_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 apg_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 throughCREATE OR REPLACEinensure_schema(), so the fix reaches the database only when a component that runsensure_schema()starts on 0.5.33 (restarting an older component reverts to the old definition; after a mixed-version period, check withpg_get_functiondef).
0.5.32 (2026-08-12)
- Added the guardrail alert writers (
GuardrailAlertWriter/GuardrailAlertAggregates) — public producer writers for the admin triage viewguardrail_alerts(a non-authoritative operational view; the action-audit ledger remains authoritative) and the unlinked daily aggregatesguardrail_alert_daily_aggregates. Structurally enforced: rows have no end-user ID column, and SelfHarm is rejected at row construction with a synchronousValueError(normalizing spelling variants and str-Enum.value) and recorded in the aggregates only. Rows are idempotent throughevent_keyv2 (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 taggedaction_audit_spill) /record_sync()= audit-before-act (raisesActionAuditWriteErrorwhen 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.*requiresactor_ref(synchronousValueError). No extra dependencies (stdlib only, part of core).
0.5.30 (2026-08-07)
- Fix:
run_marketplace_agent/arun_marketplace_agentalways failed withAttributeErrorwhendata_accesswas omitted — thedb_config/PostgreSQLConfig.from_env()fallback passed a rawPostgreSQLConfigtoDataAccessand crashed with'PostgreSQLConfig' object has no attribute 'initialize'before the agent ran (so the minimalrun_marketplace_agent(my_agent)documented in 0.5.29 always failed). The config is now turned into a manager withcreate_postgresql_manager(config)before connecting. When api_url (DB_API_PROXY_URL) is set, the runner cannot supply a token_provider and raisesMarketplaceRunnerConfigErrorexplicitly. 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_IDetc.) is missing, the runner stops withMarketplaceRunnerConfigErrorwithout 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)
boto3moved from[security]to[security-aws]— 0.5.25 added boto3 to[security]forAWSSecurityClient, which pulled boto3 back into lightweight installs that pin[storage-azure,security]and deliberately leave it out. To use the AWS content-safety backends, installpip 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_platformsucceeds on a core install —__init__.pyimported every module unconditionally, sopip install agenticstar-platform(core) failed withModuleNotFoundError: asyncpgand[db]failed withModuleNotFoundError: openaiat 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_handlerdetect 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.0added to the core dependencies — theauthmodule uses it, but it was undeclared and[all]only worked by accident through transitive dependencies.boto3added to[security]— required byAWSSecurityClient(Comprehend / Bedrock Guardrails) but undeclared.[all]includes the[security]dependencies (google-cloud-dlp/google-cloud-aiplatform) — previouslyGCPSecurityClientcould 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:
EmbeddingConfiggains aproviderfield ("azure"default /"openai") —provider = "openai"targets OpenAI-compatible endpoints such as embed-v-4-0 on Azure AI inference (base_urlmust include/models;api_versionis 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()passesopenai_base_urlfor the openai provider — when[memory.embedder]uses anopenai/...model withbase_urlset, the Mem0 embedder now connects to that endpoint. ⚠️ Without this release, the base_url was silently dropped and the client connected toapi.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 singledocumentsarray (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.SecurityClientBasegains 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 withRATE_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 witherror_code="RATE_LIMITED", letting callers surface429 + Retry-Afterto 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
documentsanderrors) 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) —
GCPSecurityClientcan now moderate content via Vertex AI safety filters across all Marketplace regions, with a Gemini-based semantic judge as an additional layer. Addsgoogle-cloud-aiplatform>=1.60.0to 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:
QdrantConfigErrorfolded into theVectorStoreErrorhierarchy;initialize()is idempotent — init-failure wrapping now passes already-typed exceptions through (config errors are no longer mislabeled), and callinginitialize()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_onereturningNoneon 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=...)+ newmissing_reasoncolumn onllm_usage_ledger— the usage-missing reason (provider_omitted/stream_interrupted/ …) is now stored on the row (previously logs only), so offline exports can distinguishcost_usd IS NULL(unpriced) vsusage_status='missing'vs true-zero. Present rows storeNULL(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_usdhelper +UsageMeter.recordstores cost as a roundedDecimal— avoids float-repr drift (0.00089999…) in thecost_usdnumeric column; 8-dp rounding does not change billing sums, andNaN/Infare stored asNULL. Exported from bothagenticstar_platformandagenticstar_platform.metering.extract_usagereads top-levelcache_read_input_tokensas an additionalcached_tokensfallback (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_fnfallback applies prompt-cache pricing — passescache_read_input_tokens/cache_creation_input_tokenstolitellm.cost_per_tokenwhencompletion_cost(response)is unavailable, so cache-heavy calls are no longer priced at the full input rate.extract_usagecovers Responses API + cache-creation — readscached_tokensfrominput_tokens_details(Responses) as well asprompt_tokens_details(Chat), and propagatescache_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) —
AWSSecurityConfiggains apii_servicefield ("bedrock_guardrails"default /"comprehend"legacy). AWSdetect_pii()routes through Bedrock GuardrailssensitiveInformationPolicyby default, fixing multilingual (including Japanese) PII masking — Amazon ComprehendDetectPiiEntitiesonly supportsen/es(previouslyjaetc. slipped through and raised aValidationException). ⚠️ Behavior change: with the new default, AWS PII requiresguardrail_idto be set (otherwise it returnsNOT_CONFIGURED); setpii_service="comprehend"to keep the legacy en/es path. Configs that already setpii_serviceexplicitly 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 stdlibtomllib(no externaltomldependency).host = "${POSTGRESQL_HOST}"-style values inconfig.tomlare 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.xfilters/top_kinternally (the publicuser_id/limitarguments are unchanged). Azure gpt-5.x / o-series memory models no longer fail withmax_tokens is not supported(the unsupported parameter is suppressed; the real Azure deployment is still targeted).mem0aiis 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 SSEconsume_events()loop, calldrain()so handlers actually fire (previouslyemit+cleanupsilently dropped events). The[webhook]extra now includesaiohttp(required byWebhookEventHandler);[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 withdownload_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 raisingModuleNotFoundError.
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 tollm_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 — defaultdefault_cost_fnuses litellm if installed (long-context / cache / tier aware), gracefully records tokens-only (cost_usd = NULL) when litellm is absent, andcost_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 optionalconfidence_thresholdparameter on Azure / AWS / GCP clients (and theSecurityClientProtocol/SecurityClientBase). PassingNonefalls back to the value in*SecurityConfig.pii_confidence_threshold. This lets a single long-livedSecurityClientinstance 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 aLIKELY(likelihood ≥ 4) cutoff and ignoredGCPSecurityConfig.pii_confidence_threshold. It now compareslikelihood / 5.0against the configured threshold, matching Azure / AWS behavior. With the defaultpii_confidence_threshold = 0.7the effective cutoff stays at likelihood ≥ 4, so most callers see no change. Callers that had setpii_confidence_thresholdbelow 0.7 will start seeing additionalPOSSIBLE(likelihood 3) findings. - Reuse the client to avoid leaks —
AzureSecurityClient(and AWS/GCP equivalents) hold anhttpx.AsyncClient(TLS context + connection pool) internally. Construct one client per process and callawait client.close()on shutdown (or useasync with); creating a new client per request without closing leaks resources.
0.5.2 (2026-03-28)
- Memory: Removed episodic memory (Graphiti/FalkorDB) —
episodic.pywas 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 includesgraphiti-core[falkordb]. - README updated to reflect episodic memory removal.
0.5.0 (2026-03-25)
Breaking Changes:
- DB:
DataAccessnow takes a manager instance instead of(config, use_proxy, token_provider). Callers createPostgreSQLManagerorApiPostgreSQLManagerand pass it directly. - DB:
use_proxyparameter removed fromDataAccess,create_postgresql_manager(). - DB:
api_proxy_urlrenamed toapi_urlinPostgreSQLConfig. - DB:
ApiPostgreSQLManagerexported as public API for HTTP API access.
Improvements:
- DB:
is_initialized()method added to bothPostgreSQLManagerandApiPostgreSQLManager. - Qdrant:
prefer_grpc/check_compatibilityare now explicitQdrantConfigfields (no longer hardcoded based onauth_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 alanguageparameter (default:"ja") for accurate multi-language PII detection. Previously hardcoded to Japanese. - Security: Protocol/ABC updated -
SecurityClientProtocolandSecurityClientBaseupdated withlanguageparameter indetect_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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| agenticstar_platform-3.1.3.tar.gz | 1.0 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.