planar 0.12.0__py3-none-any.whl → 0.13.2__py3-none-any.whl

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.
planar/ai/__init__.py CHANGED
@@ -6,10 +6,12 @@ from .agent_utils import (
6
6
  agent_configuration,
7
7
  get_agent_config,
8
8
  )
9
+ from .tool_context import get_tool_context
9
10
 
10
11
  __all__ = [
11
12
  "Agent",
12
13
  "AgentRunResult",
13
14
  "agent_configuration",
14
15
  "get_agent_config",
16
+ "get_tool_context",
15
17
  ]
planar/ai/agent.py CHANGED
@@ -50,8 +50,8 @@ class AgentWorkflowNotifier(AgentEventEmitter):
50
50
  class Agent[
51
51
  TInput: BaseModel | str,
52
52
  TOutput: BaseModel | str,
53
- TDeps,
54
- ](AgentBase[TInput, TOutput, TDeps]):
53
+ TToolContext,
54
+ ](AgentBase[TInput, TOutput, TToolContext]):
55
55
  model: models.KnownModelName | models.Model = "openai:gpt-4o"
56
56
 
57
57
  async def run_step(
planar/ai/agent_base.py CHANGED
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import abc
4
2
  from dataclasses import dataclass, field
5
3
  from typing import (
@@ -15,7 +13,7 @@ from pydantic import BaseModel
15
13
  from pydantic_ai.settings import ModelSettings
16
14
 
17
15
  from planar.ai.models import AgentConfig, AgentEventEmitter, AgentRunResult
18
- from planar.ai.state import delete_state, set_state
16
+ from planar.ai.tool_context import clear_tool_context, set_tool_context
19
17
  from planar.logging import get_logger
20
18
  from planar.modeling.field_helpers import JsonSchema
21
19
  from planar.utils import P, R, T, U
@@ -30,7 +28,7 @@ class AgentBase[
30
28
  # TODO: add `= str` default when we upgrade to 3.13
31
29
  TInput: BaseModel | str,
32
30
  TOutput: BaseModel | str,
33
- TState,
31
+ TToolContext,
34
32
  ](abc.ABC):
35
33
  """An LLM-powered agent that can be called directly within workflows."""
36
34
 
@@ -47,7 +45,7 @@ class AgentBase[
47
45
  )
48
46
  event_emitter: AgentEventEmitter | None = None
49
47
  durable: bool = True
50
- state_type: Type[TState] | None = None
48
+ tool_context_type: Type[TToolContext] | None = None
51
49
 
52
50
  # TODO: move here to serialize to frontend
53
51
  #
@@ -94,16 +92,16 @@ class AgentBase[
94
92
 
95
93
  @overload
96
94
  async def __call__(
97
- self: "AgentBase[TInput, str, TState]",
95
+ self: "AgentBase[TInput, str, TToolContext]",
98
96
  input_value: TInput,
99
- state: TState | None = None,
97
+ tool_context: TToolContext | None = None,
100
98
  ) -> AgentRunResult[str]: ...
101
99
 
102
100
  @overload
103
101
  async def __call__(
104
- self: "AgentBase[TInput, TOutput, TState]",
102
+ self: "AgentBase[TInput, TOutput, TToolContext]",
105
103
  input_value: TInput,
106
- state: TState | None = None,
104
+ tool_context: TToolContext | None = None,
107
105
  ) -> AgentRunResult[TOutput]: ...
108
106
 
109
107
  def as_step_if_durable(
@@ -125,7 +123,7 @@ class AgentBase[
125
123
  async def __call__(
126
124
  self,
127
125
  input_value: TInput,
128
- state: TState | None = None,
126
+ tool_context: TToolContext | None = None,
129
127
  ) -> AgentRunResult[Any]:
130
128
  if self.input_type is not None and not isinstance(input_value, self.input_type):
131
129
  raise ValueError(
@@ -153,22 +151,25 @@ class AgentBase[
153
151
  return_type=AgentRunResult[self.output_type],
154
152
  )
155
153
 
156
- if state is not None:
157
- if self.state_type is None:
158
- raise ValueError("state cannot be provided when state_type is not set")
159
- if not isinstance(state, self.state_type):
154
+ if tool_context is not None:
155
+ if self.tool_context_type is None:
156
+ raise ValueError(
157
+ "tool_context cannot be provided when tool_context_type is not set"
158
+ )
159
+ if not isinstance(tool_context, self.tool_context_type):
160
160
  raise ValueError(
161
- f"state must be of type {self.state_type}, but got {type(state)}"
161
+ f"tool_context must be of type {self.tool_context_type}, "
162
+ f"but got {type(tool_context)}"
162
163
  )
163
- set_state(cast(TState, state))
164
+ set_tool_context(cast(TToolContext, tool_context))
164
165
 
165
166
  try:
166
167
  result = await run_step(input_value=input_value)
167
168
  # Cast the result to ensure type compatibility
168
169
  return cast(AgentRunResult[TOutput], result)
169
170
  finally:
170
- if state is not None:
171
- delete_state()
171
+ if tool_context is not None:
172
+ clear_tool_context()
172
173
 
173
174
  @abc.abstractmethod
174
175
  async def run_step(
@@ -5,13 +5,13 @@ from planar.task_local import TaskLocal
5
5
  data: TaskLocal[Any] = TaskLocal()
6
6
 
7
7
 
8
- def set_state(ctx: Any):
8
+ def set_tool_context(ctx: Any):
9
9
  return data.set(ctx)
10
10
 
11
11
 
12
- def get_state[T](_: Type[T]) -> T:
12
+ def get_tool_context[T](_: Type[T]) -> T:
13
13
  return cast(T, data.get())
14
14
 
15
15
 
16
- def delete_state():
16
+ def clear_tool_context():
17
17
  return data.clear()
planar/cli.py CHANGED
@@ -262,6 +262,11 @@ def run_command(
262
262
  def scaffold_project(
263
263
  name: str = typer.Option(None, "--name", help="Name of the new project"),
264
264
  directory: Path = typer.Option(Path("."), "--directory", help="Target directory"),
265
+ use_local_source: bool = typer.Option(
266
+ False,
267
+ "--use-local-source",
268
+ help="Use local planar source instead of published package (for development)",
269
+ ),
265
270
  ):
266
271
  """
267
272
  Creates a new Planar project with a basic structure and example workflow.
@@ -278,8 +283,19 @@ def scaffold_project(
278
283
  template_dir = Path(__file__).parent / "scaffold_templates"
279
284
  jinja_env = JinjaEnvironment(loader=FileSystemLoader(template_dir))
280
285
 
286
+ planar_source_path = None
287
+ if use_local_source:
288
+ # Assume we're running from the planar package itself
289
+ # Go up from planar/cli.py -> planar/ -> planar_repo/
290
+ planar_source_path = Path(__file__).parent.parent.resolve()
291
+ typer.echo(f"Using local planar source: {planar_source_path}")
292
+
281
293
  # Template context
282
- context = {"name": name}
294
+ context = {
295
+ "name": name,
296
+ "local_source": use_local_source,
297
+ "planar_source_path": planar_source_path,
298
+ }
283
299
 
284
300
  # Create project structure
285
301
  try:
planar/config.py CHANGED
@@ -3,6 +3,7 @@ import logging
3
3
  import logging.config
4
4
  import os
5
5
  import sys
6
+ from contextlib import contextmanager
6
7
  from enum import Enum
7
8
  from pathlib import Path
8
9
  from typing import Annotated, Any, Dict, Literal
@@ -24,6 +25,7 @@ from sqlalchemy import URL, make_url
24
25
  from planar.data.config import DataConfig
25
26
  from planar.files.storage.config import LocalDirectoryConfig, StorageConfig
26
27
  from planar.logging import get_logger
28
+ from planar.logging.formatter import StructuredFormatter
27
29
 
28
30
  logger = get_logger(__name__)
29
31
 
@@ -144,12 +146,12 @@ class CorsConfig(BaseModel):
144
146
  allow_headers: list[str]
145
147
 
146
148
  @model_validator(mode="after")
147
- def validate_allow_origins(cls, instance):
148
- if instance.allow_credentials and "*" in instance.allow_origins:
149
+ def validate_allow_origins(self):
150
+ if self.allow_credentials and "*" in self.allow_origins:
149
151
  raise ValueError(
150
152
  "allow_credentials cannot be True if allow_origins includes '*'. Must explicitly specify allowed origins."
151
153
  )
152
- return instance
154
+ return self
153
155
 
154
156
 
155
157
  LOCAL_CORS_CONFIG = CorsConfig(
@@ -173,10 +175,10 @@ class JWTConfig(BaseModel):
173
175
  additional_exclusion_paths: list[str] | None = Field(default_factory=list)
174
176
 
175
177
  @model_validator(mode="after")
176
- def validate_client_id(cls, instance):
177
- if not instance.client_id or not instance.org_id:
178
+ def validate_client_id(self):
179
+ if not self.client_id or not self.org_id:
178
180
  raise ValueError("Both client_id and org_id required to enable JWT")
179
- return instance
181
+ return self
180
182
 
181
183
 
182
184
  # Coplane ORG JWT config
@@ -235,12 +237,12 @@ class PlanarConfig(BaseModel):
235
237
  model_config = ConfigDict(extra="forbid")
236
238
 
237
239
  @model_validator(mode="after")
238
- def validate_db_connection_reference(cls, instance):
239
- if instance.app.db_connection not in instance.db_connections:
240
+ def validate_db_connection_reference(self):
241
+ if self.app.db_connection not in self.db_connections:
240
242
  raise ValueError(
241
- f"Invalid db_connection reference: {instance.app.db_connection}"
243
+ f"Invalid db_connection reference: {self.app.db_connection}"
242
244
  )
243
- return instance
245
+ return self
244
246
 
245
247
  def connection_url(self) -> URL:
246
248
  connection = self.db_connections[self.app.db_connection]
@@ -460,6 +462,28 @@ def load_environment_aware_env_vars() -> None:
460
462
  return
461
463
 
462
464
 
465
+ @contextmanager
466
+ def _temporary_config_logging():
467
+ """Install a console handler so config loading logs are visible before configuration."""
468
+ root_logger = logging.getLogger()
469
+ handler = logging.StreamHandler(sys.stderr)
470
+ handler.setFormatter(StructuredFormatter(use_colors=sys.stderr.isatty()))
471
+ handler.setLevel(logging.NOTSET)
472
+
473
+ previous_level = root_logger.level
474
+ if root_logger.getEffectiveLevel() > logging.INFO:
475
+ root_logger.setLevel(logging.INFO)
476
+
477
+ root_logger.addHandler(handler)
478
+
479
+ try:
480
+ yield
481
+ finally:
482
+ root_logger.removeHandler(handler)
483
+ handler.close()
484
+ root_logger.setLevel(previous_level)
485
+
486
+
463
487
  def load_environment_aware_config[ConfigClass]() -> PlanarConfig:
464
488
  """
465
489
  Load configuration based on environment settings, using environment variables
@@ -476,83 +500,87 @@ def load_environment_aware_config[ConfigClass]() -> PlanarConfig:
476
500
  Raises:
477
501
  InvalidConfigurationError: If configuration loading or validation fails.
478
502
  """
479
- load_environment_aware_env_vars()
480
- env = get_environment()
481
-
482
- if env == "dev":
483
- base_config = sqlite_config(db_path="planar_dev.db")
484
- base_config.security = SecurityConfig(cors=LOCAL_CORS_CONFIG)
485
- base_config.environment = Environment.DEV
486
- else:
487
- base_config = sqlite_config(db_path="planar.db")
488
- base_config.environment = Environment.PROD
489
- base_config.security = SecurityConfig(
490
- cors=PROD_CORS_CONFIG, jwt=JWT_COPLANE_CONFIG
491
- )
503
+ with _temporary_config_logging():
504
+ load_environment_aware_env_vars()
505
+ env = get_environment()
506
+
507
+ if env == "dev":
508
+ base_config = sqlite_config(db_path="planar_dev.db")
509
+ base_config.security = SecurityConfig(cors=LOCAL_CORS_CONFIG)
510
+ base_config.environment = Environment.DEV
511
+ else:
512
+ base_config = sqlite_config(db_path="planar.db")
513
+ base_config.environment = Environment.PROD
514
+ base_config.security = SecurityConfig(
515
+ cors=PROD_CORS_CONFIG, jwt=JWT_COPLANE_CONFIG
516
+ )
492
517
 
493
- # Convert base config to dict for merging
494
- # Use by_alias=False to work with Python field names before validation
495
- base_dict = base_config.model_dump(mode="python", by_alias=False)
518
+ # Convert base config to dict for merging
519
+ # Use by_alias=False to work with Python field names before validation
520
+ base_dict = base_config.model_dump(mode="python", by_alias=False)
496
521
 
497
- override_config_path = get_config_path()
498
- if override_config_path:
499
- if not override_config_path.exists():
500
- raise InvalidConfigurationError(
501
- f"Configuration file not found: {override_config_path}"
522
+ override_config_path = get_config_path()
523
+ if override_config_path:
524
+ if not override_config_path.exists():
525
+ raise InvalidConfigurationError(
526
+ f"Configuration file not found: {override_config_path}"
527
+ )
528
+ else:
529
+ paths_to_check = []
530
+ if os.environ.get("PLANAR_ENTRY_POINT"):
531
+ # Extract the directory from the entry point path
532
+ entry_point_dir = Path(os.environ["PLANAR_ENTRY_POINT"]).parent
533
+ paths_to_check = [
534
+ entry_point_dir / f"planar.{env}.yaml",
535
+ entry_point_dir / "planar.yaml",
536
+ ]
537
+ paths_to_check.append(Path(f"planar.{env}.yaml"))
538
+ paths_to_check.append(Path("planar.yaml"))
539
+
540
+ override_config_path = next(
541
+ (path for path in paths_to_check if path.exists()), None
502
542
  )
503
- else:
504
- paths_to_check = []
505
- if os.environ.get("PLANAR_ENTRY_POINT"):
506
- # Extract the directory from the entry point path
507
- entry_point_dir = Path(os.environ["PLANAR_ENTRY_POINT"]).parent
508
- paths_to_check = [
509
- entry_point_dir / f"planar.{env}.yaml",
510
- entry_point_dir / "planar.yaml",
511
- ]
512
- paths_to_check.append(Path(f"planar.{env}.yaml"))
513
- paths_to_check.append(Path("planar.yaml"))
514
-
515
- override_config_path = next(
516
- (path for path in paths_to_check if path.exists()), None
517
- )
518
- if override_config_path is None:
519
- logger.warning(
520
- "no override config file found, using default config",
521
- search_paths=[str(p) for p in paths_to_check],
522
- env=env,
543
+ if override_config_path is None:
544
+ logger.warning(
545
+ "no override config file found, using default config",
546
+ search_paths=[str(p) for p in paths_to_check],
547
+ env=env,
548
+ )
549
+
550
+ merged_dict = base_dict
551
+ if override_config_path and override_config_path.exists():
552
+ logger.info(
553
+ "using override config file", override_config_path=override_config_path
523
554
  )
555
+ try:
556
+ # We can't use load_config_from_file here because we expect
557
+ # the override config to not be a fully validated PlanarConfig object,
558
+ # and we need to merge it onto the base default config.
559
+ with open(override_config_path, "r") as f:
560
+ override_yaml_str = f.read()
561
+
562
+ # Expand environment variables in the YAML string
563
+ processed_yaml_str = os.path.expandvars(override_yaml_str)
564
+ logger.debug(
565
+ "processed override yaml string",
566
+ processed_yaml_str=processed_yaml_str,
567
+ )
568
+
569
+ override_dict = yaml.safe_load(processed_yaml_str) or {}
570
+ logger.debug("loaded override config", override_dict=override_dict)
571
+
572
+ # Deep merge the override onto the base dictionary
573
+ merged_dict = deep_merge_dicts(override_dict, base_dict)
574
+ logger.debug("merged config dict", merged_dict=merged_dict)
575
+ except yaml.YAMLError as e:
576
+ raise InvalidConfigurationError(
577
+ f"Error parsing override configuration file {override_config_path}: {e}"
578
+ ) from e
524
579
 
525
- merged_dict = base_dict
526
- if override_config_path and override_config_path.exists():
527
- logger.info(
528
- "using override config file", override_config_path=override_config_path
529
- )
530
580
  try:
531
- # We can't use load_config_from_file here because we expect
532
- # the override config to not be a fully validated PlanarConfig object,
533
- # and we need to merge it onto the base default config.
534
- with open(override_config_path, "r") as f:
535
- override_yaml_str = f.read()
536
-
537
- # Expand environment variables in the YAML string
538
- processed_yaml_str = os.path.expandvars(override_yaml_str)
539
- logger.debug(
540
- "processed override yaml string", processed_yaml_str=processed_yaml_str
541
- )
542
-
543
- override_dict = yaml.safe_load(processed_yaml_str) or {}
544
- logger.debug("loaded override config", override_dict=override_dict)
545
-
546
- # Deep merge the override onto the base dictionary
547
- merged_dict = deep_merge_dicts(override_dict, base_dict)
548
- logger.debug("merged config dict", merged_dict=merged_dict)
549
- except yaml.YAMLError as e:
581
+ final_config = PlanarConfig.model_validate(merged_dict)
582
+ return final_config
583
+ except ValidationError as e:
550
584
  raise InvalidConfigurationError(
551
- f"Error parsing override configuration file {override_config_path}: {e}"
585
+ f"Configuration validation error: {e}"
552
586
  ) from e
553
-
554
- try:
555
- final_config = PlanarConfig.model_validate(merged_dict)
556
- return final_config
557
- except ValidationError as e:
558
- raise InvalidConfigurationError(f"Configuration validation error: {e}") from e
planar/data/connection.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  from dataclasses import dataclass
3
+ from urllib.parse import urlparse
3
4
 
4
5
  import ibis
5
6
  from ibis.backends.duckdb import Backend as DuckDBBackend
@@ -55,6 +56,48 @@ async def _close_backend(connection: DuckDBBackend) -> None:
55
56
  logger.warning("failed to close DuckDB connection", error=str(exc))
56
57
 
57
58
 
59
+ def _make_aws_s3_secret_query(config: S3Config) -> str:
60
+ """
61
+ https://duckdb.org/docs/stable/core_extensions/httpfs/s3api
62
+ """
63
+ columns = ["TYPE s3"]
64
+
65
+ if config.region:
66
+ columns.append(
67
+ f"REGION '{config.region}'",
68
+ )
69
+
70
+ if config.endpoint_url:
71
+ parsed_url = urlparse(config.endpoint_url)
72
+ endpoint_host = parsed_url.hostname
73
+ endpoint_port = f":{parsed_url.port}" if parsed_url.port else ""
74
+ endpoint = f"{endpoint_host}{endpoint_port}"
75
+
76
+ columns.append(f"ENDPOINT '{endpoint}'")
77
+
78
+ if config.access_key and config.secret_key:
79
+ columns.extend(
80
+ [
81
+ "PROVIDER config",
82
+ f"KEY_ID '{config.access_key}'",
83
+ f"SECRET '{config.secret_key}'",
84
+ ]
85
+ )
86
+ else:
87
+ columns.extend(
88
+ [
89
+ "PROVIDER credential_chain",
90
+ "CHAIN 'env;sts;instance'",
91
+ ]
92
+ )
93
+
94
+ return f"""
95
+ CREATE OR REPLACE SECRET secret (
96
+ {", ".join(columns)}
97
+ );
98
+ """
99
+
100
+
58
101
  async def _create_connection(config: PlanarConfig) -> DuckDBBackend:
59
102
  """Create Ibis DuckDB connection with Ducklake."""
60
103
  data_config = config.data
@@ -105,6 +148,14 @@ async def _create_connection(config: PlanarConfig) -> DuckDBBackend:
105
148
  if isinstance(storage, LocalDirectoryConfig):
106
149
  data_path = storage.directory
107
150
  elif isinstance(storage, S3Config):
151
+ await asyncio.to_thread(con.raw_sql, "INSTALL httpfs;")
152
+ await asyncio.to_thread(con.raw_sql, "LOAD httpfs;")
153
+
154
+ await asyncio.to_thread(
155
+ con.raw_sql,
156
+ _make_aws_s3_secret_query(storage),
157
+ )
158
+
108
159
  data_path = f"s3://{storage.bucket_name}/"
109
160
  else:
110
161
  # Generic fallback
planar/human/__init__.py CHANGED
@@ -1,2 +1,11 @@
1
- from .human import Human, Timeout, complete_human_task, get_human_tasks # noqa: F401
2
- from .models import HumanTask, HumanTaskResult # noqa: F401
1
+ from .human import Human, Timeout, complete_human_task, get_human_tasks
2
+ from .models import HumanTask, HumanTaskResult
3
+
4
+ __all__ = [
5
+ "Human",
6
+ "Timeout",
7
+ "complete_human_task",
8
+ "get_human_tasks",
9
+ "HumanTask",
10
+ "HumanTaskResult",
11
+ ]
planar/human/models.py CHANGED
@@ -7,7 +7,6 @@ from pydantic import BaseModel
7
7
  from sqlmodel import JSON, Column, Field
8
8
 
9
9
  from planar.db import PlanarInternalBase
10
- from planar.modeling.field_helpers import JsonSchema
11
10
  from planar.modeling.mixins import TimestampMixin
12
11
  from planar.modeling.mixins.auditable import AuditableMixin
13
12
  from planar.modeling.mixins.uuid_primary_key import UUIDPrimaryKeyMixin
@@ -48,12 +47,12 @@ class HumanTask(
48
47
  workflow_name: str
49
48
 
50
49
  # Input data for context
51
- input_schema: Optional[JsonSchema] = Field(default=None, sa_column=Column(JSON))
50
+ input_schema: Optional[dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
52
51
  input_data: Optional[dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
53
52
  message: Optional[str] = Field(default=None)
54
53
 
55
54
  # Schema for expected output
56
- output_schema: JsonSchema = Field(sa_column=Column(JSON))
55
+ output_schema: dict[str, Any] = Field(sa_column=Column(JSON))
57
56
  output_data: Optional[dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
58
57
 
59
58
  # Suggested data for the form (optional)
@@ -18,6 +18,7 @@ def _in_event_loop_task() -> bool:
18
18
  return (
19
19
  threading.main_thread() == threading.current_thread()
20
20
  and asyncio.get_running_loop() is not None
21
+ and asyncio.current_task() is not None
21
22
  )
22
23
  except RuntimeError:
23
24
  return False
@@ -3,8 +3,16 @@ name = "{{ name }}"
3
3
  version = "0.1.0"
4
4
  requires-python = ">=3.12"
5
5
  dependencies = [
6
- "planar[data]>=0.10.0",
6
+ {%- if local_source %}
7
+ "planar",
8
+ {%- else %}
9
+ "planar>=0.10.0",
10
+ {%- endif %}
7
11
  ]
8
12
 
9
- [[tool.uv.index]]
10
- url = "https://coplane.github.io/planar/simple/"
13
+ {%- if local_source %}
14
+
15
+ [tool.uv.sources]
16
+ planar = { path = "{{ planar_source_path }}", editable = true }
17
+
18
+ {%- endif %}
planar/sse/proxy.py CHANGED
@@ -178,7 +178,13 @@ class SSEProxy:
178
178
  # builtin hub. That's why we check for self.builtin_process instead of
179
179
  # self.enable_builtin_hub. Also helps with type checking.
180
180
  logger.info("terminating builtin sse hub process")
181
- self.builtin_process.terminate()
181
+ try:
182
+ self.builtin_process.terminate()
183
+ except AttributeError:
184
+ # Race condition: _popen may be None if process already terminated.
185
+ # This can happen when stop() is called multiple times during shutdown.
186
+ logger.debug("process already terminated or cleaned up")
187
+ self.builtin_process = None
182
188
 
183
189
  def setup_proxy_endpoint(self):
184
190
  @self.router.get("/")
@@ -104,11 +104,15 @@ def agent_notify(kind: Notification, data: str):
104
104
  if callback is not None:
105
105
  context = get_context()
106
106
  logger.debug("notifying agent event", kind=kind)
107
+ if context.step_stack:
108
+ step_id = context.step_stack[-1].step_id
109
+ else:
110
+ step_id = -1
107
111
  callback(
108
112
  WorkflowNotification(
109
113
  kind=kind,
110
114
  workflow_id=context.workflow_id,
111
- data=AgentEventData(data=data, step_id=context.step_stack[-1].step_id),
115
+ data=AgentEventData(data=data, step_id=step_id),
112
116
  )
113
117
  )
114
118
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: planar
3
- Version: 0.12.0
3
+ Version: 0.13.2
4
4
  Summary: Batteries-included framework for building durable agentic workflows and business applications.
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  Requires-Dist: aiofiles>=24.1.0
@@ -25,6 +25,7 @@ Requires-Dist: zen-engine>=0.40.0
25
25
  Requires-Dist: azure-storage-blob>=12.19.0 ; extra == 'azure'
26
26
  Requires-Dist: azure-identity>=1.15.0 ; extra == 'azure'
27
27
  Requires-Dist: aiohttp>=3.8.0 ; extra == 'azure'
28
+ Requires-Dist: duckdb>=1.3,<1.4 ; extra == 'data'
28
29
  Requires-Dist: ducklake>=0.1.1 ; extra == 'data'
29
30
  Requires-Dist: ibis-framework[duckdb]>=10.8.0 ; extra == 'data'
30
31
  Requires-Dist: polars>=1.31.0 ; extra == 'data'
@@ -1,18 +1,18 @@
1
1
  planar/__init__.py,sha256=FAYRGjuJOH2Y_XYFA0-BrRFjuKdPzIShNbaYwJbtu6A,499
2
- planar/ai/__init__.py,sha256=ABOKvqQOLlVJkptcvXcuLjVZZWEsK8h-1RyFGK7kib8,231
3
- planar/ai/agent.py,sha256=HAyQvdA-NOEYMtze3Gh_BtOzk-rD1DXnv1jNGWd-9dA,12499
4
- planar/ai/agent_base.py,sha256=5UURilfMldIYoTNzwlSY9eVaub5LDovCGFeAszyuGWE,6198
2
+ planar/ai/__init__.py,sha256=lgGmY3ffmKqt35JncZmJOK5jWZ0bJRAksR54hYqV8is,298
3
+ planar/ai/agent.py,sha256=xNJiQV-FC1BVkIBb9zpIy8NKN7jQl1j21TMiACyxBZg,12513
4
+ planar/ai/agent_base.py,sha256=0P4vjTvka8MERP9z360J4nGd-TAfeafK9zczbhcvU9k,6410
5
5
  planar/ai/agent_utils.py,sha256=MYNerdAm2TPVbDSKAmBCUlGmR56NAc8seZmDAFOWvUA,4199
6
6
  planar/ai/models.py,sha256=bZd4MoBBJMqzXJqsmsbMdZtOaRrNeX438CHAqOvmpfw,4598
7
7
  planar/ai/pydantic_ai.py,sha256=FpD0pE7wWNYwmEUZ90D7_J8gbAoqKmWtrLr2fhAd7rg,23503
8
- planar/ai/state.py,sha256=6vQ8VMLxJYB75QXkm8AVPkdXHUMwwjKxBWH-hGIK9p0,278
8
+ planar/ai/tool_context.py,sha256=9W7VFrHPhNeieEvlJlv-OrFPVzySItdUjKCutkXpYFU,298
9
9
  planar/ai/utils.py,sha256=WVBW0TGaoKytC4bNd_a9lXrBf5QsDRut4GBcA53U2Ww,3116
10
10
  planar/app.py,sha256=2ijbrORCfjKReDieLbwgey_9poJJwEfWkvCNKcVYcdk,19484
11
- planar/cli.py,sha256=9LINDDiAUBDmzWQY9BuCYTssM7eb-ypa5bL9HAY3Ld8,10500
12
- planar/config.py,sha256=6J42G9rEVUiOyCAY3EwUTU3PPmWthGTnrHMzST9TMcc,17809
11
+ planar/cli.py,sha256=GONw7Dpyxy_7Vf48rLmQXRQnC_IyWENRULQF0DwiCuY,11108
12
+ planar/config.py,sha256=54N-qPaxihEn6QUQvXsdtbEN-pTTrd_5NG8WFYb0CRI,18879
13
13
  planar/data/__init__.py,sha256=eqSREzJ58HM05DXpR_00M6RDFxtHSIh-OEuqXBPVsVc,362
14
14
  planar/data/config.py,sha256=oaLiwN3OUt3i85MS0zxpdMNG5VjaM5xB6VtPc1f4MFU,1484
15
- planar/data/connection.py,sha256=OQL-EEBtfEOtN5PYZA5TswCTD8Cebt9-pP_w5Qifs24,6705
15
+ planar/data/connection.py,sha256=QPcbomoWDdttu28cfeQAtnUdc_ptayQTaqwcC8Gq4vc,8056
16
16
  planar/data/dataset.py,sha256=f-x9cOuGyQQldC98mCwpFVOXMiDNN6LQDQ9vtljXVRo,6095
17
17
  planar/data/exceptions.py,sha256=AlhGQ_TReyEzfPSlqoXCjoZ1206Ut7dS4lrukVfGHaw,358
18
18
  planar/data/utils.py,sha256=NhxJFSxPLNX1ddSANS3-bVhMr1Q_-25qDRRXxGug4Vc,6312
@@ -32,11 +32,11 @@ planar/files/storage/config.py,sha256=lzODgufhZliwQDikqMQCX3KKiap0CL6Cc9qbIdeUMC
32
32
  planar/files/storage/context.py,sha256=Qadlz4T-FQ1A0TdGsOfuNmM679ohayU7oaexUpT8AY0,361
33
33
  planar/files/storage/local_directory.py,sha256=1SsEfr0Ud9TvSQJneDl_M-D7AFYixLE9t-bVIiY3aSI,7395
34
34
  planar/files/storage/s3.py,sha256=1861rSw3kplXtugUWD7mdSD_EnPSHME1mGc82V69r5g,8234
35
- planar/human/__init__.py,sha256=FwpV-FFssKKlvKSjWoI4gJB1XTMaNb1UNCSBxjAtIBw,147
35
+ planar/human/__init__.py,sha256=SOdDlIvf4QOz_wGhJ_w-7v6b1daJHbO785TsJUVAzOc,252
36
36
  planar/human/human.py,sha256=-oRtN_8bCtSV7Sxku7yG4rof7T5pr4j18Cfm3u4Z3PM,14925
37
- planar/human/models.py,sha256=Cec1Y9NGGtuAl1ZhqNc9PWIq__BbiWVTh7IYKR4yl3w,2317
37
+ planar/human/models.py,sha256=uYqH79qgwH2HM6BO7dR54WFcvleVCfpZOYRSNJZ9P2o,2272
38
38
  planar/logging/__init__.py,sha256=BW101gskQS3C2agKSDiJEy4L-j5z6BP1AwEfd2JMXHM,254
39
- planar/logging/attributes.py,sha256=SB-lC-Osa55zXkX2UAWm56vKXvHgbF7TmhGnSiBWejk,1529
39
+ planar/logging/attributes.py,sha256=4JCmncF46MbGt6q4wD60DZss8eBWDdKDvxS8w8lE9fc,1580
40
40
  planar/logging/context.py,sha256=mGVpL-276VWpRUIGlkGwbn9gkATqFS7lcFn1aM0v-eQ,384
41
41
  planar/logging/formatter.py,sha256=NMDhpo7ezsfGO7cPwOWbClqODviEgSjqHCoqrCCIu2c,3560
42
42
  planar/logging/logger.py,sha256=vj3-z2ea0J1ppgDpJBsrueA3jg5kjk9ceIo-4tWarXk,2650
@@ -81,7 +81,7 @@ planar/scaffold_templates/app/flows/process_invoice.py.j2,sha256=R3EII_O2DHV1kvf
81
81
  planar/scaffold_templates/main.py.j2,sha256=zrqsuv3Fp4lcknvB37RrRHy11msdFB1yDguYmTLLPhw,398
82
82
  planar/scaffold_templates/planar.dev.yaml.j2,sha256=I5-IqX7GJm6qA91WtUMw43L4hKACqgnER_H2racim4c,998
83
83
  planar/scaffold_templates/planar.prod.yaml.j2,sha256=FahJ2atDtvVH7IUCatGq6h9hmyF8meeiWC8RLfWphOQ,867
84
- planar/scaffold_templates/pyproject.toml.j2,sha256=HpBc3cuTr-VeVN3BQ8y1Vr2eOtkL7TLH5mNz_T-akpA,190
84
+ planar/scaffold_templates/pyproject.toml.j2,sha256=XdOf3B_fkTDwEzQsneNdmC43rptmZoqwFRj7QpaL7OI,299
85
85
  planar/security/auth_context.py,sha256=i63JkHQ3oXNlTis7GIKRkZJbkcvZhD2jVDuO7blgbSc,5068
86
86
  planar/security/auth_middleware.py,sha256=Grrm0i2bstWZ83ukrNZsHvFbNzffN0rvbbCcb2OxRY0,5746
87
87
  planar/security/authorization.py,sha256=zTh5rLmVJZnGDq540_1WYljJ7Hws-BY_P6VqUCONqLE,12760
@@ -92,7 +92,7 @@ planar/sse/constants.py,sha256=jE3SooTEWPuuL_Bi6DisJYMR9pKOiHVfboU2h5QTJRg,22
92
92
  planar/sse/example.html,sha256=SgTJbdJ3B1F1DxLC2YWuX2F1XVwKcTjX34CbJCXoCTM,4144
93
93
  planar/sse/hub.py,sha256=5jhfk7zdCivau3TT1MxU2qtvETSskhqEiXzt-t0sRpE,6859
94
94
  planar/sse/model.py,sha256=fU_Fx9LS2ouS6-Dj1TIF-PLGul9YratKWafoWfZR1gc,123
95
- planar/sse/proxy.py,sha256=aJGo_-JIeQ0xSmE4HJdulZxIgCVRsBMMXqqSqtPvTvo,9177
95
+ planar/sse/proxy.py,sha256=LgfnCjZHtnDquV5HpdoSru43pxk-T5aY9NCjQLNrrcQ,9518
96
96
  planar/task_local.py,sha256=pyvT0bdzAn15HL2yQUs9YrU5MVXh9njQt9MH51AGljs,1102
97
97
  planar/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  planar/testing/fixtures.py,sha256=z0VkKTqAg9z_1L_ojB0pTzKSaLPcDeNxRq_RB36rUU8,9740
@@ -112,7 +112,7 @@ planar/workflows/execution.py,sha256=8c4a2L1qRMPQrCEJ8-sEk-gJi_xKq5gYKDSWSbSspVI
112
112
  planar/workflows/lock.py,sha256=QU5_y_n8RHOC7ppLicH7yWX-Ni7N93hlB14D2zXOQ8A,8773
113
113
  planar/workflows/misc.py,sha256=g3XVRMeU9mgDRi_6RgFdydLEqvTAg49wbIGlmC7kOu8,140
114
114
  planar/workflows/models.py,sha256=SKBJTGhd4nVWxtlDkaQrU2RvRTtoj07PJhLT73cF_ac,5591
115
- planar/workflows/notifications.py,sha256=JrObfWD-uRZJlZLMSDJDqjDuXfYAoRSLfgEeyoy98Vs,3795
115
+ planar/workflows/notifications.py,sha256=APAUKk1-Y7R5E1LHA4RWQK5Z6vh2lIvf5tHYWwhD5WM,3895
116
116
  planar/workflows/orchestrator.py,sha256=rneB1yOPDZiJcHFbD6UDZ4juU77iSBK1eu1gOFm58vM,15480
117
117
  planar/workflows/query.py,sha256=38B5SLwXf3AlA_1ChR5DicFWdcUqzpQzMkuAUCNHafI,8838
118
118
  planar/workflows/serialization.py,sha256=v3eqUS0odUFS7PnIrKyKUrK-feIv0ssxEp2VxjkddyI,13733
@@ -122,7 +122,7 @@ planar/workflows/step_testing_utils.py,sha256=WiTwxB4mM2y6dW7CJ3PlIR1BkBodSxQV7-
122
122
  planar/workflows/sub_workflow_runner.py,sha256=EpS7DhhXRbC6ABm-Sho6Uyxh2TqCjcTPDYvcTQN4FjY,8313
123
123
  planar/workflows/tracing.py,sha256=E7E_kj2VBQisDqrllviIshbvOmB9QcEeRwMapunqio4,2732
124
124
  planar/workflows/wrappers.py,sha256=dY_3NqkzGMG4jgX2lkAqvHTYFA1lBzhkQCw7N5CyaQM,1174
125
- planar-0.12.0.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
126
- planar-0.12.0.dist-info/entry_points.txt,sha256=L3T0w9u2UPKWXv6JbXFWKU1d5xyEAq1xVWbpYS6mLNg,96
127
- planar-0.12.0.dist-info/METADATA,sha256=QzYhhuXw0VLyymqS31qadZ_DeIYbG7v93MZAe5t2Esw,7285
128
- planar-0.12.0.dist-info/RECORD,,
125
+ planar-0.13.2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
126
+ planar-0.13.2.dist-info/entry_points.txt,sha256=L3T0w9u2UPKWXv6JbXFWKU1d5xyEAq1xVWbpYS6mLNg,96
127
+ planar-0.13.2.dist-info/METADATA,sha256=hHuWQx_u5HHzw4onoBPARIX8wVjU_PwW_ia6X5g7oPU,7335
128
+ planar-0.13.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.22
2
+ Generator: uv 0.8.24
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any