plexspaces 0.1.4__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plexspaces-0.1.4 → plexspaces-0.1.6}/PKG-INFO +1 -1
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/__init__.py +6 -0
- plexspaces-0.1.6/plexspaces/agent.py +407 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/actor/v1/__init__.py +3 -3
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/metrics/v1/__init__.py +1 -2
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/node/v1/__init__.py +10 -88
- plexspaces-0.1.6/plexspaces/generated/plexspaces/security/v1/__init__.py +822 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/wasm/v1/__init__.py +1 -1
- plexspaces-0.1.6/plexspaces/generated/plexspaces/workflow/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/host.py +101 -56
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/runtime.py +25 -3
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces.egg-info/PKG-INFO +1 -1
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces.egg-info/SOURCES.txt +3 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/pyproject.toml +1 -1
- plexspaces-0.1.6/tests/test_agent.py +314 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_runtime.py +4 -3
- plexspaces-0.1.4/plexspaces/generated/plexspaces/security/v1/__init__.py +0 -254
- {plexspaces-0.1.4 → plexspaces-0.1.6}/README.md +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/actor_id.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/decorators.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/buf/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/buf/validate/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/google/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/google/api/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/grpc/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/grpc/gateway/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/grpc/gateway/protoc_gen_openapiv2/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/grpc/gateway/protoc_gen_openapiv2/options/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/actor/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/application/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/application/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/behaviors/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/behaviors/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/channel/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/channel/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/circuitbreaker/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/circuitbreaker/prv/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/common/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/common/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/config/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/config/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/dashboard/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/dashboard/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/facets/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/facets/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/firecracker/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/firecracker/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/grpc/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/grpc/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/journaling/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/journaling/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/keyvalue/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/keyvalue/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/locks/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/locks/prv/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/mailbox/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/mailbox/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/metrics/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/node/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/object_registry/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/object_registry/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/persistence/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/persistence/prv/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/pool/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/pool/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/processgroups/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/processgroups/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/scheduling/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/scheduling/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/security/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/services/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/services/prv/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/storage/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/storage/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/supervision/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/supervision/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/system/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/system/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/timer/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/timer/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/tuplespace/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/tuplespace/v1/__init__.py +0 -0
- {plexspaces-0.1.4/plexspaces/generated/plexspaces/wasm → plexspaces-0.1.6/plexspaces/generated/plexspaces/v1}/__init__.py +0 -0
- {plexspaces-0.1.4/plexspaces/generated/plexspaces/workflow → plexspaces-0.1.6/plexspaces/generated/plexspaces/wasm}/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/workflow/v1/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/leader_worker.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/proto_wire.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/workflow.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces.egg-info/dependency_links.txt +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces.egg-info/entry_points.txt +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces.egg-info/requires.txt +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces.egg-info/top_level.txt +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces_cli/__init__.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces_cli/build.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/setup.cfg +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_abstractions_contract.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_actor_id.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_application_metrics_host.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_decorators.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_host.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_host_collective.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_host_tuple_space.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_http_fetch_wire.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_lock_wire.py +0 -0
- {plexspaces-0.1.4 → plexspaces-0.1.6}/tests/test_workflow_retry.py +0 -0
|
@@ -51,6 +51,7 @@ from .decorators import (
|
|
|
51
51
|
signal_handler,
|
|
52
52
|
query_handler,
|
|
53
53
|
)
|
|
54
|
+
from .agent import AgentLoop, AgentConfig, AgentTrajectory, AgentStep, AgentStepKind
|
|
54
55
|
from .host import host, pg_first, ServiceHttpClient
|
|
55
56
|
from .workflow import default_retry_config, with_retry
|
|
56
57
|
from .leader_worker import LeaderWorkerClient, list_worker_node_ids
|
|
@@ -59,6 +60,11 @@ from .actor_id import ActorID
|
|
|
59
60
|
__all__ = [
|
|
60
61
|
"ActorID",
|
|
61
62
|
"actor",
|
|
63
|
+
"AgentConfig",
|
|
64
|
+
"AgentLoop",
|
|
65
|
+
"AgentStep",
|
|
66
|
+
"AgentStepKind",
|
|
67
|
+
"AgentTrajectory",
|
|
62
68
|
"default_retry_config",
|
|
63
69
|
"event_actor",
|
|
64
70
|
"gen_server_actor",
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
#
|
|
4
|
+
# This file is part of PlexSpaces.
|
|
5
|
+
#
|
|
6
|
+
# PlexSpaces is free software: you can redistribute it and/or modify it under
|
|
7
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
|
8
|
+
# Software Foundation, either version 3 of the License, or (at your option)
|
|
9
|
+
# any later version.
|
|
10
|
+
#
|
|
11
|
+
# PlexSpaces is distributed in the hope that it will be useful, but WITHOUT
|
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
13
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
14
|
+
# details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
# along with PlexSpaces. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
Agent harness utilities for PlexSpaces Python SDK.
|
|
21
|
+
|
|
22
|
+
``AgentLoop`` is a **standalone class** (no decorator magic, no class injection)
|
|
23
|
+
that provides structured OODA step tracking, token budget enforcement, and
|
|
24
|
+
suspend/resume helpers. Use it with ``@workflow_actor`` for durable agent
|
|
25
|
+
execution.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
from plexspaces import workflow_actor, state, run_handler, host
|
|
30
|
+
from plexspaces.agent import AgentLoop, AgentConfig
|
|
31
|
+
|
|
32
|
+
@workflow_actor
|
|
33
|
+
class ResearchAgent:
|
|
34
|
+
task: str = state(default="")
|
|
35
|
+
|
|
36
|
+
@run_handler
|
|
37
|
+
def run(self, task: str = "") -> dict:
|
|
38
|
+
loop = AgentLoop(
|
|
39
|
+
actor_id=host.actor_id(),
|
|
40
|
+
config=AgentConfig(max_iterations=10, token_budget=4096),
|
|
41
|
+
)
|
|
42
|
+
while not loop.iteration_limit_reached() and not loop.budget_exceeded():
|
|
43
|
+
obs = loop.observe({"task": task})
|
|
44
|
+
plan = loop.orient(obs)
|
|
45
|
+
action = loop.decide(plan)
|
|
46
|
+
result = loop.act(action)
|
|
47
|
+
loop.increment_iteration()
|
|
48
|
+
if result.get("done"):
|
|
49
|
+
break
|
|
50
|
+
return loop.finalize_trajectory("completed", "task done")
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import time
|
|
56
|
+
import uuid
|
|
57
|
+
from dataclasses import dataclass
|
|
58
|
+
from typing import Any, Dict, List, Optional
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Utilities
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _now_ms() -> int:
|
|
66
|
+
"""Current time as integer milliseconds since the Unix epoch."""
|
|
67
|
+
return int(time.time() * 1000)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _new_ulid() -> str:
|
|
71
|
+
"""Generate a new ULID-compatible unique string (UUID4 fallback)."""
|
|
72
|
+
return str(uuid.uuid4())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# AgentStepKind constants
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
class AgentStepKind:
|
|
80
|
+
OBSERVE = "observe"
|
|
81
|
+
ORIENT = "orient"
|
|
82
|
+
DECIDE = "decide"
|
|
83
|
+
ACT = "act"
|
|
84
|
+
TOOL_CALL = "tool_call"
|
|
85
|
+
SUSPEND = "suspend"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# AgentStep
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class AgentStep:
|
|
93
|
+
"""A single step in an agent's OODA trajectory."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, kind: str, method: str, input_data: Any = None):
|
|
96
|
+
self.step_id: str = _new_ulid()
|
|
97
|
+
self.kind: str = kind
|
|
98
|
+
self.method: str = method
|
|
99
|
+
self.input: Any = input_data
|
|
100
|
+
self.output: Any = None
|
|
101
|
+
self.started_at_ms: int = _now_ms()
|
|
102
|
+
self.completed_at_ms: Optional[int] = None
|
|
103
|
+
self.success: bool = False
|
|
104
|
+
self.error: Optional[str] = None
|
|
105
|
+
self.input_tokens: int = 0
|
|
106
|
+
self.output_tokens: int = 0
|
|
107
|
+
self.model: str = ""
|
|
108
|
+
self.metadata: Dict[str, str] = {}
|
|
109
|
+
|
|
110
|
+
def complete(
|
|
111
|
+
self,
|
|
112
|
+
output: Any,
|
|
113
|
+
input_tokens: int = 0,
|
|
114
|
+
output_tokens: int = 0,
|
|
115
|
+
model: str = "",
|
|
116
|
+
) -> "AgentStep":
|
|
117
|
+
"""Mark the step as successfully completed."""
|
|
118
|
+
self.output = output
|
|
119
|
+
self.completed_at_ms = _now_ms()
|
|
120
|
+
self.success = True
|
|
121
|
+
self.input_tokens = input_tokens
|
|
122
|
+
self.output_tokens = output_tokens
|
|
123
|
+
self.model = model
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def fail(self, error: str) -> "AgentStep":
|
|
127
|
+
"""Mark the step as failed."""
|
|
128
|
+
self.completed_at_ms = _now_ms()
|
|
129
|
+
self.success = False
|
|
130
|
+
self.error = error
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def duration_ms(self) -> int:
|
|
135
|
+
end = self.completed_at_ms or _now_ms()
|
|
136
|
+
return max(0, end - self.started_at_ms)
|
|
137
|
+
|
|
138
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
139
|
+
return {
|
|
140
|
+
"step_id": self.step_id,
|
|
141
|
+
"kind": self.kind,
|
|
142
|
+
"method": self.method,
|
|
143
|
+
"input": self.input,
|
|
144
|
+
"output": self.output,
|
|
145
|
+
"started_at_ms": self.started_at_ms,
|
|
146
|
+
"completed_at_ms": self.completed_at_ms,
|
|
147
|
+
"duration_ms": self.duration_ms,
|
|
148
|
+
"success": self.success,
|
|
149
|
+
"error": self.error,
|
|
150
|
+
"input_tokens": self.input_tokens,
|
|
151
|
+
"output_tokens": self.output_tokens,
|
|
152
|
+
"model": self.model,
|
|
153
|
+
"metadata": self.metadata,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# AgentTrajectory
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
class AgentTrajectory:
|
|
162
|
+
"""Complete trajectory for one agent run (all OODA steps, token totals)."""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
agent_actor_id: str,
|
|
167
|
+
eval_run_id: str = "",
|
|
168
|
+
scenario_id: str = "",
|
|
169
|
+
):
|
|
170
|
+
self.trajectory_id: str = _new_ulid()
|
|
171
|
+
self.agent_actor_id: str = agent_actor_id
|
|
172
|
+
self.eval_run_id: str = eval_run_id
|
|
173
|
+
self.scenario_id: str = scenario_id
|
|
174
|
+
self.steps: List[AgentStep] = []
|
|
175
|
+
self.outcome: str = ""
|
|
176
|
+
self.outcome_detail: str = ""
|
|
177
|
+
self.started_at_ms: int = _now_ms()
|
|
178
|
+
self.completed_at_ms: Optional[int] = None
|
|
179
|
+
self.total_input_tokens: int = 0
|
|
180
|
+
self.total_output_tokens: int = 0
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def duration_ms(self) -> int:
|
|
184
|
+
end = self.completed_at_ms or _now_ms()
|
|
185
|
+
return max(0, end - self.started_at_ms)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def step_count(self) -> int:
|
|
189
|
+
return len(self.steps)
|
|
190
|
+
|
|
191
|
+
def add_step(self, step: AgentStep) -> None:
|
|
192
|
+
"""Append a completed step and accumulate token totals."""
|
|
193
|
+
self.steps.append(step)
|
|
194
|
+
self.total_input_tokens += step.input_tokens
|
|
195
|
+
self.total_output_tokens += step.output_tokens
|
|
196
|
+
|
|
197
|
+
def complete(self, outcome: str, detail: str = "") -> None:
|
|
198
|
+
"""Finalise the trajectory with an outcome string."""
|
|
199
|
+
self.outcome = outcome
|
|
200
|
+
self.outcome_detail = detail
|
|
201
|
+
self.completed_at_ms = _now_ms()
|
|
202
|
+
|
|
203
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
204
|
+
return {
|
|
205
|
+
"trajectory_id": self.trajectory_id,
|
|
206
|
+
"agent_actor_id": self.agent_actor_id,
|
|
207
|
+
"eval_run_id": self.eval_run_id,
|
|
208
|
+
"scenario_id": self.scenario_id,
|
|
209
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
210
|
+
"step_count": len(self.steps),
|
|
211
|
+
"outcome": self.outcome,
|
|
212
|
+
"outcome_detail": self.outcome_detail,
|
|
213
|
+
"started_at_ms": self.started_at_ms,
|
|
214
|
+
"completed_at_ms": self.completed_at_ms,
|
|
215
|
+
"duration_ms": self.duration_ms,
|
|
216
|
+
"total_input_tokens": self.total_input_tokens,
|
|
217
|
+
"total_output_tokens": self.total_output_tokens,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# AgentConfig
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class AgentConfig:
|
|
227
|
+
"""Configuration for an AgentLoop."""
|
|
228
|
+
max_iterations: int = 10
|
|
229
|
+
token_budget: int = 0 # 0 = no budget limit
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# AgentLoop
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
class AgentLoop:
|
|
237
|
+
"""
|
|
238
|
+
Standalone agent harness utility.
|
|
239
|
+
|
|
240
|
+
Records OODA steps, enforces token budget and iteration limits, and
|
|
241
|
+
produces an ``AgentTrajectory`` on finalisation.
|
|
242
|
+
|
|
243
|
+
This is a **plain class** — there is no class injection or metaclass magic.
|
|
244
|
+
Use it inside a ``@workflow_actor`` ``run_handler``.
|
|
245
|
+
|
|
246
|
+
:param actor_id: ID of the hosting actor.
|
|
247
|
+
:param config: Optional ``AgentConfig`` (max_iterations, token_budget).
|
|
248
|
+
:param eval_run_id: Optional eval run ID to tag the trajectory.
|
|
249
|
+
:param scenario_id: Optional scenario ID to tag the trajectory.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(
|
|
253
|
+
self,
|
|
254
|
+
actor_id: str,
|
|
255
|
+
config: Optional[AgentConfig] = None,
|
|
256
|
+
eval_run_id: str = "",
|
|
257
|
+
scenario_id: str = "",
|
|
258
|
+
):
|
|
259
|
+
self._actor_id = actor_id
|
|
260
|
+
self._config = config or AgentConfig()
|
|
261
|
+
self._trajectory = AgentTrajectory(
|
|
262
|
+
agent_actor_id=actor_id,
|
|
263
|
+
eval_run_id=eval_run_id,
|
|
264
|
+
scenario_id=scenario_id,
|
|
265
|
+
)
|
|
266
|
+
self._iteration_count: int = 0
|
|
267
|
+
self.is_suspended: bool = False
|
|
268
|
+
|
|
269
|
+
# ── Private helpers ──────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
def _record(
|
|
272
|
+
self,
|
|
273
|
+
kind: str,
|
|
274
|
+
input_data: Any,
|
|
275
|
+
output: Any = None,
|
|
276
|
+
input_tokens: int = 0,
|
|
277
|
+
output_tokens: int = 0,
|
|
278
|
+
model: str = "",
|
|
279
|
+
error: Optional[str] = None,
|
|
280
|
+
) -> AgentStep:
|
|
281
|
+
started = _now_ms()
|
|
282
|
+
step = AgentStep(kind=kind, method=kind, input_data=input_data)
|
|
283
|
+
step.started_at_ms = started
|
|
284
|
+
if error:
|
|
285
|
+
step.fail(error)
|
|
286
|
+
else:
|
|
287
|
+
step.complete(
|
|
288
|
+
output=output,
|
|
289
|
+
input_tokens=input_tokens,
|
|
290
|
+
output_tokens=output_tokens,
|
|
291
|
+
model=model,
|
|
292
|
+
)
|
|
293
|
+
self._trajectory.add_step(step)
|
|
294
|
+
return step
|
|
295
|
+
|
|
296
|
+
# ── OODA step methods ─────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def observe(self, input_data: Any, **kwargs: Any) -> Any:
|
|
299
|
+
"""Record an OBSERVE step. Returns *input_data* unchanged."""
|
|
300
|
+
self._record(AgentStepKind.OBSERVE, input_data, output=input_data, **kwargs)
|
|
301
|
+
return input_data
|
|
302
|
+
|
|
303
|
+
def orient(self, input_data: Any, **kwargs: Any) -> Any:
|
|
304
|
+
"""Record an ORIENT step. Returns *input_data* unchanged."""
|
|
305
|
+
self._record(AgentStepKind.ORIENT, input_data, output=input_data, **kwargs)
|
|
306
|
+
return input_data
|
|
307
|
+
|
|
308
|
+
def decide(self, input_data: Any, **kwargs: Any) -> Any:
|
|
309
|
+
"""Record a DECIDE step. Returns *input_data* unchanged."""
|
|
310
|
+
self._record(AgentStepKind.DECIDE, input_data, output=input_data, **kwargs)
|
|
311
|
+
return input_data
|
|
312
|
+
|
|
313
|
+
def act(
|
|
314
|
+
self,
|
|
315
|
+
input_data: Any,
|
|
316
|
+
output: Any = None,
|
|
317
|
+
input_tokens: int = 0,
|
|
318
|
+
output_tokens: int = 0,
|
|
319
|
+
model: str = "",
|
|
320
|
+
**kwargs: Any,
|
|
321
|
+
) -> Any:
|
|
322
|
+
"""Record an ACT step. Returns *output* (or *input_data* if output is None)."""
|
|
323
|
+
result = output if output is not None else input_data
|
|
324
|
+
self._record(
|
|
325
|
+
AgentStepKind.ACT,
|
|
326
|
+
input_data,
|
|
327
|
+
output=result,
|
|
328
|
+
input_tokens=input_tokens,
|
|
329
|
+
output_tokens=output_tokens,
|
|
330
|
+
model=model,
|
|
331
|
+
)
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
def tool_call(
|
|
335
|
+
self,
|
|
336
|
+
name: str,
|
|
337
|
+
args: Any,
|
|
338
|
+
result: Any,
|
|
339
|
+
started_at_ms: Optional[int] = None,
|
|
340
|
+
input_tokens: int = 0,
|
|
341
|
+
output_tokens: int = 0,
|
|
342
|
+
model: str = "",
|
|
343
|
+
) -> Any:
|
|
344
|
+
"""Record a TOOL_CALL step. Returns *result*."""
|
|
345
|
+
step = AgentStep(
|
|
346
|
+
kind=AgentStepKind.TOOL_CALL,
|
|
347
|
+
method=name,
|
|
348
|
+
input_data=args,
|
|
349
|
+
)
|
|
350
|
+
if started_at_ms is not None:
|
|
351
|
+
step.started_at_ms = started_at_ms
|
|
352
|
+
step.complete(
|
|
353
|
+
output=result,
|
|
354
|
+
input_tokens=input_tokens,
|
|
355
|
+
output_tokens=output_tokens,
|
|
356
|
+
model=model,
|
|
357
|
+
)
|
|
358
|
+
self._trajectory.add_step(step)
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
# ── Control ───────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
def suspend(self, reason: str) -> None:
|
|
364
|
+
"""Mark the agent as suspended (waiting for external signal)."""
|
|
365
|
+
self.is_suspended = True
|
|
366
|
+
self._record(AgentStepKind.SUSPEND, reason)
|
|
367
|
+
|
|
368
|
+
def increment_iteration(self) -> None:
|
|
369
|
+
"""Increment the OODA cycle counter. Call once per full OODA cycle."""
|
|
370
|
+
self._iteration_count += 1
|
|
371
|
+
|
|
372
|
+
def budget_exceeded(self) -> bool:
|
|
373
|
+
"""True if the cumulative token count meets or exceeds the budget."""
|
|
374
|
+
budget = self._config.token_budget
|
|
375
|
+
if budget <= 0:
|
|
376
|
+
return False
|
|
377
|
+
total = (
|
|
378
|
+
self._trajectory.total_input_tokens
|
|
379
|
+
+ self._trajectory.total_output_tokens
|
|
380
|
+
)
|
|
381
|
+
return total >= budget
|
|
382
|
+
|
|
383
|
+
def iteration_limit_reached(self) -> bool:
|
|
384
|
+
"""True if the OODA cycle count meets or exceeds max_iterations.
|
|
385
|
+
|
|
386
|
+
Returns False when max_iterations is 0 (unlimited), matching Go/TS/Rust.
|
|
387
|
+
"""
|
|
388
|
+
if self._config.max_iterations <= 0:
|
|
389
|
+
return False
|
|
390
|
+
return self._iteration_count >= self._config.max_iterations
|
|
391
|
+
|
|
392
|
+
# ── Trajectory accessors ──────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def get_trajectory(self) -> AgentTrajectory:
|
|
395
|
+
"""Return the in-progress ``AgentTrajectory`` object."""
|
|
396
|
+
return self._trajectory
|
|
397
|
+
|
|
398
|
+
def finalize_trajectory(self, outcome: str, detail: str = "") -> Dict[str, Any]:
|
|
399
|
+
"""
|
|
400
|
+
Finalise the trajectory with an outcome and return it as a dict.
|
|
401
|
+
|
|
402
|
+
:param outcome: "completed", "error", "timeout", "budget_exceeded", etc.
|
|
403
|
+
:param detail: Human-readable detail.
|
|
404
|
+
:returns: ``AgentTrajectory.to_dict()``
|
|
405
|
+
"""
|
|
406
|
+
self._trajectory.complete(outcome=outcome, detail=detail)
|
|
407
|
+
return self._trajectory.to_dict()
|
|
@@ -607,8 +607,8 @@ class Actor(betterproto.Message):
|
|
|
607
607
|
|
|
608
608
|
## Migration Strategy
|
|
609
609
|
```
|
|
610
|
-
Version 1 → 2: Add new field with default value (
|
|
611
|
-
Version 2 → 3: Remove
|
|
610
|
+
Version 1 → 2: Add new field with default value (no migration needed)
|
|
611
|
+
Version 2 → 3: Remove field (no migration needed)
|
|
612
612
|
Version 3 → 4: Change field type (BREAKING - requires explicit migration function)
|
|
613
613
|
Version 4 → 5: Restructure nested fields (BREAKING - requires migration)
|
|
614
614
|
```
|
|
@@ -771,7 +771,7 @@ class ActorConfig(betterproto.Message):
|
|
|
771
771
|
// Version 1: Simple timeout config
|
|
772
772
|
properties: {"timeout_ms": 5000}
|
|
773
773
|
|
|
774
|
-
// Version 2: Added retry config
|
|
774
|
+
// Version 2: Added retry config
|
|
775
775
|
properties: {
|
|
776
776
|
"timeout_ms": 5000,
|
|
777
777
|
"max_retries": 3, // NEW: defaults to 3
|
{plexspaces-0.1.4 → plexspaces-0.1.6}/plexspaces/generated/plexspaces/metrics/v1/__init__.py
RENAMED
|
@@ -278,8 +278,7 @@ class ActorMetrics(betterproto.Message):
|
|
|
278
278
|
|
|
279
279
|
## Purpose
|
|
280
280
|
Tracks actor lifecycle and message routing/delivery metrics that can be updated by ActorRegistry
|
|
281
|
-
without depending on Node. These metrics are also synced to NodeMetrics
|
|
282
|
-
via NodeMetricsUpdater for backward compatibility.
|
|
281
|
+
without depending on Node. These metrics are also synced to NodeMetrics via NodeMetricsUpdater.
|
|
283
282
|
|
|
284
283
|
## Design
|
|
285
284
|
Proto-first design: All actor/message metrics are defined in proto for:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
2
|
-
# sources: plexspaces/v1/node/node.proto, plexspaces/v1/node/outbound.proto, plexspaces/v1/node/release.proto
|
|
2
|
+
# sources: plexspaces/v1/node/node.proto, plexspaces/v1/node/outbound.proto, plexspaces/v1/node/release.proto
|
|
3
3
|
# plugin: python-betterproto
|
|
4
4
|
# This file has been @generated
|
|
5
5
|
|
|
@@ -148,93 +148,6 @@ class MemberState(betterproto.Enum):
|
|
|
148
148
|
LEFT = 4
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
class ShutdownErrorCode(betterproto.Enum):
|
|
152
|
-
"""Shutdown coordinator error codes"""
|
|
153
|
-
|
|
154
|
-
SHUTDOWN_ERROR_UNSPECIFIED = 0
|
|
155
|
-
SHUTDOWN_ERROR_PHASE_FAILED = 1
|
|
156
|
-
"""A shutdown phase failed (phase name + error in message)"""
|
|
157
|
-
|
|
158
|
-
SHUTDOWN_ERROR_TIMEOUT = 2
|
|
159
|
-
"""Shutdown sequence timed out"""
|
|
160
|
-
|
|
161
|
-
SHUTDOWN_ERROR_OTHER = 3
|
|
162
|
-
"""Other shutdown error"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
class HealthCheckErrorCode(betterproto.Enum):
|
|
166
|
-
"""Health check error codes"""
|
|
167
|
-
|
|
168
|
-
HEALTH_CHECK_ERROR_UNSPECIFIED = 0
|
|
169
|
-
HEALTH_CHECK_ERROR_CHECK_FAILED = 1
|
|
170
|
-
"""A health check returned a failure result"""
|
|
171
|
-
|
|
172
|
-
HEALTH_CHECK_ERROR_TIMEOUT = 2
|
|
173
|
-
"""Health check did not complete within deadline"""
|
|
174
|
-
|
|
175
|
-
HEALTH_CHECK_ERROR_OTHER = 3
|
|
176
|
-
"""Unexpected error running a health check"""
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
class BackgroundSchedulerErrorCode(betterproto.Enum):
|
|
180
|
-
"""Background scheduler error codes"""
|
|
181
|
-
|
|
182
|
-
BACKGROUND_SCHEDULER_ERROR_UNSPECIFIED = 0
|
|
183
|
-
BACKGROUND_SCHEDULER_ERROR_ALREADY_STARTED = 1
|
|
184
|
-
"""Scheduler already running for this node in the current process"""
|
|
185
|
-
|
|
186
|
-
BACKGROUND_SCHEDULER_ERROR_LOCK_ERROR = 2
|
|
187
|
-
"""Lock manager error (lease acquisition)"""
|
|
188
|
-
|
|
189
|
-
BACKGROUND_SCHEDULER_ERROR_CHANNEL_ERROR = 3
|
|
190
|
-
"""Internal channel error"""
|
|
191
|
-
|
|
192
|
-
BACKGROUND_SCHEDULER_ERROR_STATE_STORE_ERROR = 4
|
|
193
|
-
"""State store error"""
|
|
194
|
-
|
|
195
|
-
BACKGROUND_SCHEDULER_ERROR_NODE_SELECTION_ERROR = 5
|
|
196
|
-
"""Node selection error during scheduling"""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class CapacityTrackerErrorCode(betterproto.Enum):
|
|
200
|
-
"""Capacity tracker error codes"""
|
|
201
|
-
|
|
202
|
-
CAPACITY_TRACKER_ERROR_UNSPECIFIED = 0
|
|
203
|
-
CAPACITY_TRACKER_ERROR_REGISTRY_ERROR = 1
|
|
204
|
-
"""ObjectRegistry error reading capacity data"""
|
|
205
|
-
|
|
206
|
-
CAPACITY_TRACKER_ERROR_INVALID_DATA = 2
|
|
207
|
-
"""Capacity data is malformed or out of range"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
class NodeSelectionErrorCode(betterproto.Enum):
|
|
211
|
-
"""Node selection error codes"""
|
|
212
|
-
|
|
213
|
-
NODE_SELECTION_ERROR_UNSPECIFIED = 0
|
|
214
|
-
NODE_SELECTION_ERROR_NO_MATCHING_NODE = 1
|
|
215
|
-
"""No node matched the resource requirements"""
|
|
216
|
-
|
|
217
|
-
NODE_SELECTION_ERROR_INVALID_REQUIREMENTS = 2
|
|
218
|
-
"""Resource requirements are invalid or contradictory"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
class TaskRouterErrorCode(betterproto.Enum):
|
|
222
|
-
"""Task router error codes"""
|
|
223
|
-
|
|
224
|
-
TASK_ROUTER_ERROR_UNSPECIFIED = 0
|
|
225
|
-
TASK_ROUTER_ERROR_GROUP_NOT_FOUND = 1
|
|
226
|
-
"""Shard group not found"""
|
|
227
|
-
|
|
228
|
-
TASK_ROUTER_ERROR_NO_ACTORS_IN_GROUP = 2
|
|
229
|
-
"""Shard group has no actors registered"""
|
|
230
|
-
|
|
231
|
-
TASK_ROUTER_ERROR_CHANNEL_ERROR = 3
|
|
232
|
-
"""Internal channel error"""
|
|
233
|
-
|
|
234
|
-
TASK_ROUTER_ERROR_INVALID_STRATEGY = 4
|
|
235
|
-
"""Routing strategy is not valid for this request"""
|
|
236
|
-
|
|
237
|
-
|
|
238
151
|
@dataclass(eq=False, repr=False)
|
|
239
152
|
class HttpRetryPolicy(betterproto.Message):
|
|
240
153
|
"""
|
|
@@ -979,6 +892,15 @@ class SecurityConfig(betterproto.Message):
|
|
|
979
892
|
Never use in production - always keep auth enabled.
|
|
980
893
|
"""
|
|
981
894
|
|
|
895
|
+
oidc: "__security_v1__.OidcConfig" = betterproto.message_field(6)
|
|
896
|
+
"""
|
|
897
|
+
OIDC/OAuth2 configuration for dashboard UI login.
|
|
898
|
+
|
|
899
|
+
Only active when disable_auth = false. In non-auth mode everyone is admin.
|
|
900
|
+
Supports any OIDC-compliant provider (Google, GitHub, Okta, Auth0, etc.)
|
|
901
|
+
Client secret should be set via PLEXSPACES_OIDC_CLIENT_SECRET env var.
|
|
902
|
+
"""
|
|
903
|
+
|
|
982
904
|
|
|
983
905
|
@dataclass(eq=False, repr=False)
|
|
984
906
|
class ApplicationConfig(betterproto.Message):
|