agent-learning 0.4.2__tar.gz → 0.5.0__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.
Files changed (97) hide show
  1. {agent_learning-0.4.2/src/agent_learning.egg-info → agent_learning-0.5.0}/PKG-INFO +13 -5
  2. agent_learning-0.5.0/PYPI.md +23 -0
  3. {agent_learning-0.4.2 → agent_learning-0.5.0}/README.md +23 -12
  4. {agent_learning-0.4.2 → agent_learning-0.5.0}/pyproject.toml +1 -1
  5. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/_version.py +1 -1
  6. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/cli.py +264 -6
  7. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/base.py +3 -1
  8. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/cosmos.py +8 -0
  9. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/local.py +4 -0
  10. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/memory.py +4 -0
  11. {agent_learning-0.4.2 → agent_learning-0.5.0/src/agent_learning.egg-info}/PKG-INFO +13 -5
  12. agent_learning-0.5.0/tests/test_cli.py +648 -0
  13. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_storage_local.py +3 -0
  14. agent_learning-0.4.2/PYPI.md +0 -15
  15. agent_learning-0.4.2/tests/test_cli.py +0 -320
  16. {agent_learning-0.4.2 → agent_learning-0.5.0}/LICENSE +0 -0
  17. {agent_learning-0.4.2 → agent_learning-0.5.0}/setup.cfg +0 -0
  18. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/__init__.py +0 -0
  19. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/capture.py +0 -0
  20. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/__init__.py +0 -0
  21. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/base.py +0 -0
  22. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/router.py +0 -0
  23. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/__init__.py +0 -0
  24. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/_base.py +0 -0
  25. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/adherence.py +0 -0
  26. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/completion.py +0 -0
  27. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/intent.py +0 -0
  28. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/config.py +0 -0
  29. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/learners/__init__.py +0 -0
  30. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/learners/base.py +0 -0
  31. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/learners/reinforce.py +0 -0
  32. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/__init__.py +0 -0
  33. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/base.py +0 -0
  34. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/intent_resolution.py +0 -0
  35. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/local.py +0 -0
  36. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/registry.py +0 -0
  37. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/task_adherence.py +0 -0
  38. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/task_completion.py +0 -0
  39. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/__init__.py +0 -0
  40. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/base.py +0 -0
  41. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/contextual_softmax.py +0 -0
  42. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/softmax_bandit.py +0 -0
  43. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/py.typed +0 -0
  44. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/rewards/__init__.py +0 -0
  45. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/rewards/shaping.py +0 -0
  46. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/rewards/writer.py +0 -0
  47. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/__init__.py +0 -0
  48. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/base.py +0 -0
  49. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/__init__.py +0 -0
  50. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/_base.py +0 -0
  51. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/adherence.py +0 -0
  52. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/completion.py +0 -0
  53. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/intent.py +0 -0
  54. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/__init__.py +0 -0
  55. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/_base.py +0 -0
  56. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/adherence.py +0 -0
  57. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/completion.py +0 -0
  58. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/intent.py +0 -0
  59. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/__init__.py +0 -0
  60. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/_base.py +0 -0
  61. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/adherence.py +0 -0
  62. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/completion.py +0 -0
  63. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/intent.py +0 -0
  64. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/__init__.py +0 -0
  65. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/_base.py +0 -0
  66. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/adherence.py +0 -0
  67. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/completion.py +0 -0
  68. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/intent.py +0 -0
  69. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/__init__.py +0 -0
  70. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/_text.py +0 -0
  71. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/adherence.py +0 -0
  72. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/completion.py +0 -0
  73. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/intent.py +0 -0
  74. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/__init__.py +0 -0
  75. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/training/__init__.py +0 -0
  76. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/training/runner.py +0 -0
  77. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/types.py +0 -0
  78. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/SOURCES.txt +0 -0
  79. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/dependency_links.txt +0 -0
  80. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/entry_points.txt +0 -0
  81. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/requires.txt +0 -0
  82. {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/top_level.txt +0 -0
  83. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_capture.py +0 -0
  84. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_contextual_policy.py +0 -0
  85. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_default_store.py +0 -0
  86. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_end_to_end.py +0 -0
  87. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_learner.py +0 -0
  88. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_metrics_local.py +0 -0
  89. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_policy.py +0 -0
  90. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers.py +0 -0
  91. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_llm.py +0 -0
  92. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_nlp_text.py +0 -0
  93. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_slm.py +0 -0
  94. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_stdlib.py +0 -0
  95. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_shaping.py +0 -0
  96. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_storage_memory.py +0 -0
  97. {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-learning
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Native, in-process reinforcement learning SDK for AI agents — in-memory and local-file storage by default.
5
5
  Author: Chris Tava
6
6
  License: MIT License
@@ -67,16 +67,24 @@ Dynamic: license-file
67
67
 
68
68
  # agent-learning
69
69
 
70
- Native reinforcement learning SDK for AI agents. An in-process learner optimizes a small, interpretable policy over discrete agent choices (understand intent and complete task by choosing the right outcome).
70
+ Native reinforcement learning SDK for AI agents. An in-process Learner optimizes a small, interpretable TaskPolicy over discrete agent choices (understand intent and complete task by choosing the right outcome).
71
+
72
+ TaskPolicies model reusable decisions among executable alternatives such as
73
+ models, skills, tools, workflows, or workloads. Factual questions, ordinary
74
+ chat, reporting, and learning automation are not policy tasks.
71
75
 
72
76
  ## How it works
73
77
 
74
78
  The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tune jobs and no opaque update cycles — just three pieces that run in your existing Python process:
75
79
 
76
- 1. The **policy** is a softmax distribution over `N` discrete actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
80
+ 1. **TaskPolicy** is a softmax distribution over `N` discrete actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
81
+
82
+ 2. **Score** evaluates each episode on-device with three stdlib scorers for intent resolution, task adherence, and task completion. Their scores are combined into a single scalar reward with no scoring endpoint or environment variables required. Azure AI evaluators remain available as an opt-in.
77
83
 
78
- 2. Each episode is **evaluated** on-device by three stdlib scorers for intent resolution, task adherence, and task completion. Their scores are combined into a single scalar reward with no scoring endpoint or environment variables required. Azure AI evaluators remain available as an opt-in.
84
+ 3. **Learner** applies REINFORCE-with-baseline to update TaskPolicy logits directly from logged episodes. Updates are tiny gradient steps that run on local compute and persist through a pluggable store — in-memory or local files by default, with Azure Cosmos DB optional.
79
85
 
80
- 3. A **REINFORCE-with-baseline** learner updates the policy logits directly from logged episodes. Updates are tiny gradient steps that run on local compute and persist through a pluggable store — in-memory or local files by default, with Azure Cosmos DB optional.
86
+ `task-policy-decide` closes the loop at execution time by returning the selected
87
+ action plus historical correctness, reward, result summaries, and per-metric
88
+ quality feedback for the agent to use on its next delegated decision.
81
89
 
82
90
  Every episode, reward, run, and deployment is captured by the configured store — in-memory or local files by default, or Azure Cosmos DB — giving you a complete lineage and audit trail of how the policy evolved over time.
@@ -0,0 +1,23 @@
1
+ # agent-learning
2
+
3
+ Native reinforcement learning SDK for AI agents. An in-process Learner optimizes a small, interpretable TaskPolicy over discrete agent choices (understand intent and complete task by choosing the right outcome).
4
+
5
+ TaskPolicies model reusable decisions among executable alternatives such as
6
+ models, skills, tools, workflows, or workloads. Factual questions, ordinary
7
+ chat, reporting, and learning automation are not policy tasks.
8
+
9
+ ## How it works
10
+
11
+ The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tune jobs and no opaque update cycles — just three pieces that run in your existing Python process:
12
+
13
+ 1. **TaskPolicy** is a softmax distribution over `N` discrete actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
14
+
15
+ 2. **Score** evaluates each episode on-device with three stdlib scorers for intent resolution, task adherence, and task completion. Their scores are combined into a single scalar reward with no scoring endpoint or environment variables required. Azure AI evaluators remain available as an opt-in.
16
+
17
+ 3. **Learner** applies REINFORCE-with-baseline to update TaskPolicy logits directly from logged episodes. Updates are tiny gradient steps that run on local compute and persist through a pluggable store — in-memory or local files by default, with Azure Cosmos DB optional.
18
+
19
+ `task-policy-decide` closes the loop at execution time by returning the selected
20
+ action plus historical correctness, reward, result summaries, and per-metric
21
+ quality feedback for the agent to use on its next delegated decision.
22
+
23
+ Every episode, reward, run, and deployment is captured by the configured store — in-memory or local files by default, or Azure Cosmos DB — giving you a complete lineage and audit trail of how the policy evolved over time.
@@ -1,36 +1,45 @@
1
1
  # agent-learning
2
2
 
3
3
  Native reinforcement learning SDK for AI agents. An in-process
4
- learner optimizes a small, interpretable policy over discrete agent choices (e.g., "take action A", "take action B", "take action C") using on-device evaluation scores as the reward
4
+ Learner optimizes a small, interpretable TaskPolicy over discrete agent choices (e.g., "take action A", "take action B", "take action C") using on-device evaluation scores as the reward
5
5
  signal by default.
6
6
 
7
+ TaskPolicies represent reusable **decisions among executable alternatives**.
8
+ They are not conversation logs: factual questions, ordinary chat, reporting,
9
+ and agent-learning automation are not policy tasks.
10
+
7
11
  <p align="center">
8
- <img src="images/agent-learning-loop.svg" alt="Animated loop: Policy chooses an action, Score evaluates the episode, and Learner updates the policy" width="960" style="max-width:100%; height:auto;" />
12
+ <img src="images/agent-learning-loop.svg" alt="Animated TaskPolicy Score Learner loop: TaskPolicy chooses a task action, Score evaluates the episode, and Learner updates TaskPolicy" width="960" style="max-width:100%; height:auto;" />
9
13
  </p>
10
14
 
11
15
  ## How it works
12
16
 
13
17
  The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tune jobs and no opaque update cycles — just three pieces that run in your existing Python process:
14
18
 
15
- 1. The **policy** is a softmax distribution over `N` discrete
19
+ 1. **TaskPolicy** is a softmax distribution over `N` discrete
16
20
  actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
17
21
 
18
- <img src="images/0f85e08d0c47cd01.png" alt="Policy selects one of N discrete actions" width="360" style="max-width:100%; height:auto;" />
22
+ <img src="images/0f85e08d0c47cd01.png" alt="TaskPolicy selects one of N discrete actions" width="360" style="max-width:100%; height:auto;" />
19
23
 
20
- 2. Each episode is **evaluated** locally by three stdlib scorers for intent
24
+ 2. **Score** evaluates each episode locally with three stdlib scorers for intent
21
25
  resolution, task adherence, and task completion. Their scores are combined
22
26
  into one scalar reward. No scoring endpoint or environment variable is
23
27
  required. Configured Azure AI evaluators remain available as an opt-in.
24
28
 
25
29
  <img src="images/246d112f995b785a.png" alt="Three evaluator scores feed a single scalar reward" width="360" style="max-width:100%; height:auto;" />
26
30
 
27
- 3. A **Reinforce-with-baseline** learner updates the policy logits
31
+ 3. **Learner** applies REINFORCE-with-baseline to update TaskPolicy logits
28
32
  directly from stored episodes. Updates are tiny gradient steps
29
33
  that run on local compute and persist through a pluggable store — in-memory
30
34
  or local files by default, with Azure Cosmos DB optional.
31
35
 
32
36
  <img src="images/cc970c453583c982.png" alt="Policy quality improves with every batch of episodes" width="360" style="max-width:100%; height:auto;" />
33
37
 
38
+ Before the next delegated execution, `task-policy-decide` samples the learned
39
+ policy and returns the selected action together with correctness rate, mean
40
+ reward, recent result summaries, and intent/adherence/completion scores. Agents
41
+ consume that feedback rather than training a policy that is never used.
42
+
34
43
  Every episode, reward, run, and deployment is captured by the
35
44
  configured store — in-memory or local files by default, or Azure Cosmos DB —
36
45
  giving you a complete lineage and audit trail of how the policy
@@ -71,12 +80,14 @@ The `agent-learn` CLI provides the current task-learning-loop operations:
71
80
 
72
81
  ```text
73
82
  agent-learn list
74
- agent-learn tasks-list <agent_id>
75
- agent-learn task-episodes-count <agent_id> [--task-id <task_id>]
76
- agent-learn task-episodes-list <agent_id> [--task-id <task_id>] [--limit <1-500>] [--include-incomplete]
77
- agent-learn task-policy-init --agent-id <agent_id> --task-id <task_id> --actions ./actions.json
78
- agent-learn task-episode-register --agent-id <agent_id> --task-id <task_id> --episode ./episode.json
83
+ agent-learn --version
84
+ agent-learn tasks-list <agent_id> [--decision-only]
85
+ agent-learn task-episodes-count <agent_id> [--task-id <task_id>] [--start-date <date>] [--end-date <date>]
86
+ agent-learn task-episodes-list <agent_id> [--task-id <task_id>] [--limit <1-500>] [--include-incomplete] [--start-date <date>] [--end-date <date>]
87
+ agent-learn task-policy-init --agent-id <agent_id> --task-id <task_id> --decision-context <context> --actions ./actions.json
88
+ agent-learn task-policy-decide --agent-id <agent_id> --task-id <task_id> [--history-limit <1-500>] [--greedy] [--seed <integer>]
89
+ agent-learn task-episode-register --agent-id <agent_id> --task-id <task_id> --episode ./episode.json [--require-decision-policy]
79
90
  agent-learn score --agent-id <agent_id> [--task-id <task_id>] [--limit <1-500>]
80
- agent-learn train --agent-id <agent_id> [--task-id <task_id>] [--limit <1-500>] [--start-date <date>] [--end-date <date>] [--skip-scoring]
91
+ agent-learn train --agent-id <agent_id> [--task-id <task_id>] [--decision-only] [--limit <1-500>] [--min-episodes <1-500>] [--start-date <date>] [--end-date <date>] [--skip-scoring]
81
92
  agent-learn task-policy --agent-id <agent_id> --task-id <task_id>
82
93
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-learning"
7
- version = "0.4.2"
7
+ version = "0.5.0"
8
8
  description = "Native, in-process reinforcement learning SDK for AI agents — in-memory and local-file storage by default."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """Package version."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.5.0"
@@ -5,11 +5,15 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import logging
8
+ import math
9
+ import random
8
10
  import sys
9
11
  import uuid
10
12
  from collections import Counter
13
+ from datetime import datetime, timezone
11
14
  from typing import Any
12
15
 
16
+ from ._version import __version__
13
17
  from .policy.softmax_bandit import SoftmaxPolicy
14
18
  from .storage.cosmos import get_default_store
15
19
  from .training.runner import LearningRunner
@@ -18,6 +22,7 @@ from .types import Action, Episode, MetricName, PolicySnapshot, RewardSource
18
22
  logger = logging.getLogger(__name__)
19
23
 
20
24
  _MAX_EPISODES = 500
25
+ _DECISION_POLICY_SCOPE = "delegated_decision"
21
26
 
22
27
 
23
28
  def _episode_limit(value: str) -> int:
@@ -27,14 +32,29 @@ def _episode_limit(value: str) -> int:
27
32
  return limit
28
33
 
29
34
 
35
+ def _iso_date(value: str) -> str:
36
+ try:
37
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
38
+ except ValueError as exc:
39
+ raise argparse.ArgumentTypeError(f"invalid ISO 8601 date: {value!r}") from exc
40
+ if parsed.tzinfo is None:
41
+ parsed = parsed.replace(tzinfo=timezone.utc)
42
+ return parsed.astimezone(timezone.utc).isoformat()
43
+
44
+
30
45
  def _build_arg_parser() -> argparse.ArgumentParser:
31
- parser = argparse.ArgumentParser(prog="agent-learn", description="Native RL CLI for AI agents.")
46
+ parser = argparse.ArgumentParser(
47
+ prog="agent-learn",
48
+ description=f"Native RL CLI for AI agents. SDK version {__version__}.",
49
+ )
50
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
32
51
  sub = parser.add_subparsers(dest="command", required=True)
33
52
 
34
53
  sub.add_parser("list", help="List discovered agent ids and names.")
35
54
 
36
55
  tasks = sub.add_parser("tasks-list", help="List tasks for an agent.")
37
56
  tasks.add_argument("agent_id")
57
+ tasks.add_argument("--decision-only", action="store_true")
38
58
 
39
59
  count = sub.add_parser(
40
60
  "task-episodes-count",
@@ -42,6 +62,8 @@ def _build_arg_parser() -> argparse.ArgumentParser:
42
62
  )
43
63
  count.add_argument("agent_id")
44
64
  count.add_argument("--task-id")
65
+ count.add_argument("--start-date", type=_iso_date)
66
+ count.add_argument("--end-date", type=_iso_date)
45
67
 
46
68
  episodes = sub.add_parser(
47
69
  "task-episodes-list",
@@ -51,13 +73,17 @@ def _build_arg_parser() -> argparse.ArgumentParser:
51
73
  episodes.add_argument("--task-id")
52
74
  episodes.add_argument("--limit", type=_episode_limit, default=_MAX_EPISODES)
53
75
  episodes.add_argument("--include-incomplete", action="store_true")
76
+ episodes.add_argument("--start-date", type=_iso_date)
77
+ episodes.add_argument("--end-date", type=_iso_date)
54
78
 
55
79
  train = sub.add_parser("train", help="Run one offline learning batch.")
56
80
  train.add_argument("--agent-id", required=True)
57
81
  train.add_argument("--task-id")
58
82
  train.add_argument("--limit", type=_episode_limit, default=200)
59
- train.add_argument("--start-date")
60
- train.add_argument("--end-date")
83
+ train.add_argument("--min-episodes", type=_episode_limit, default=1)
84
+ train.add_argument("--decision-only", action="store_true")
85
+ train.add_argument("--start-date", type=_iso_date)
86
+ train.add_argument("--end-date", type=_iso_date)
61
87
  train.add_argument(
62
88
  "--skip-scoring",
63
89
  action="store_true",
@@ -73,12 +99,27 @@ def _build_arg_parser() -> argparse.ArgumentParser:
73
99
  show.add_argument("--agent-id", required=True)
74
100
  show.add_argument("--task-id", required=True)
75
101
 
102
+ decide = sub.add_parser(
103
+ "task-policy-decide",
104
+ help="Choose a delegated decision action and return learned feedback.",
105
+ )
106
+ decide.add_argument("--agent-id", required=True)
107
+ decide.add_argument("--task-id", required=True)
108
+ decide.add_argument("--history-limit", type=_episode_limit, default=100)
109
+ decide.add_argument("--greedy", action="store_true")
110
+ decide.add_argument("--seed", type=int)
111
+
76
112
  init = sub.add_parser(
77
113
  "task-policy-init",
78
114
  help="Create and activate the initial policy for an agent task.",
79
115
  )
80
116
  init.add_argument("--agent-id", required=True)
81
117
  init.add_argument("--task-id", required=True)
118
+ init.add_argument(
119
+ "--decision-context",
120
+ required=True,
121
+ help="Stable description of the delegated choice this policy controls.",
122
+ )
82
123
  init.add_argument(
83
124
  "--actions",
84
125
  required=True,
@@ -91,6 +132,7 @@ def _build_arg_parser() -> argparse.ArgumentParser:
91
132
  )
92
133
  register.add_argument("--agent-id", required=True)
93
134
  register.add_argument("--task-id", required=True)
135
+ register.add_argument("--require-decision-policy", action="store_true")
94
136
  register.add_argument(
95
137
  "--episode",
96
138
  required=True,
@@ -108,7 +150,14 @@ def _cmd_agents_list(args: argparse.Namespace) -> int:
108
150
 
109
151
 
110
152
  def _cmd_agent_tasks_list(args: argparse.Namespace) -> int:
111
- tasks = get_default_store().list_agent_tasks(args.agent_id)
153
+ store = get_default_store()
154
+ tasks = store.list_agent_tasks(args.agent_id)
155
+ if args.decision_only:
156
+ tasks = [
157
+ task
158
+ for task in tasks
159
+ if _is_decision_policy(store.get_active_policy(args.agent_id, task.id))
160
+ ]
112
161
  print(json.dumps([{"id": task.id, "name": task.name} for task in tasks], indent=2))
113
162
  return 0
114
163
 
@@ -118,6 +167,8 @@ def _cmd_agents_episodes_count(args: argparse.Namespace) -> int:
118
167
  args.agent_id,
119
168
  task_id=args.task_id,
120
169
  full_only=True,
170
+ start_date=args.start_date,
171
+ end_date=args.end_date,
121
172
  )
122
173
  print(count)
123
174
  return 0
@@ -129,6 +180,8 @@ def _cmd_agents_episodes_list(args: argparse.Namespace) -> int:
129
180
  args.agent_id,
130
181
  task_id=args.task_id,
131
182
  limit=_MAX_EPISODES,
183
+ start_date=args.start_date,
184
+ end_date=args.end_date,
132
185
  )
133
186
  if not args.include_incomplete:
134
187
  episodes = [episode for episode in episodes if episode.is_full]
@@ -179,10 +232,24 @@ def _cmd_train(args: argparse.Namespace) -> int:
179
232
  if snapshot is None:
180
233
  skipped.append({"task_id": task_id, "reason": "no active policy"})
181
234
  continue
235
+ if args.decision_only and not _is_decision_policy(snapshot):
236
+ skipped.append({"task_id": task_id, "reason": "not a delegated decision policy"})
237
+ continue
182
238
  episode_limit = episode_limits.get(task_id, 0)
183
239
  if episode_limit == 0:
184
240
  skipped.append({"task_id": task_id, "reason": "no episodes in selected batch"})
185
241
  continue
242
+ if episode_limit < args.min_episodes:
243
+ skipped.append(
244
+ {
245
+ "task_id": task_id,
246
+ "reason": (
247
+ f"selected batch has {episode_limit} episodes; "
248
+ f"minimum is {args.min_episodes}"
249
+ ),
250
+ }
251
+ )
252
+ continue
186
253
  policy = SoftmaxPolicy.from_snapshot(snapshot)
187
254
  runner = LearningRunner(store=store, policy=policy)
188
255
  run = runner.run_offline_batch(
@@ -235,6 +302,89 @@ def _policy_payload(snapshot: PolicySnapshot) -> dict[str, Any]:
235
302
  return payload
236
303
 
237
304
 
305
+ def _is_decision_policy(snapshot: PolicySnapshot | None) -> bool:
306
+ return bool(
307
+ snapshot
308
+ and snapshot.metadata.get("policy_scope") == _DECISION_POLICY_SCOPE
309
+ )
310
+
311
+
312
+ def _latest_aggregate(store: Any, episode: Episode) -> float | None:
313
+ rewards = [
314
+ reward
315
+ for reward in store.get_rewards_for_episode(episode.id, episode.agent_id)
316
+ if reward.source == RewardSource.AGGREGATE
317
+ ]
318
+ if not rewards:
319
+ return None
320
+ return max(rewards, key=lambda reward: reward.created_at).value
321
+
322
+
323
+ def _decision_feedback(
324
+ store: Any, snapshot: PolicySnapshot, history_limit: int
325
+ ) -> dict[str, Any]:
326
+ stats = {
327
+ action.id: {
328
+ "attempts": 0,
329
+ "correctness_evaluated": 0,
330
+ "correct": 0,
331
+ "correctness_rate": None,
332
+ "rewarded_episodes": 0,
333
+ "mean_reward": None,
334
+ "recent_outcomes": [],
335
+ }
336
+ for action in snapshot.actions
337
+ }
338
+ reward_totals = {action.id: 0.0 for action in snapshot.actions}
339
+ episodes = store.query_episodes(
340
+ snapshot.agent_id,
341
+ task_id=snapshot.task_id,
342
+ limit=history_limit,
343
+ )
344
+ for episode in episodes:
345
+ action_id = episode.action_id
346
+ if action_id not in stats:
347
+ continue
348
+ action_stats = stats[action_id]
349
+ action_stats["attempts"] += 1
350
+ correct_action_id = episode.metadata.get("correct_action_id")
351
+ was_correct = None
352
+ if correct_action_id:
353
+ was_correct = action_id == correct_action_id
354
+ action_stats["correctness_evaluated"] += 1
355
+ action_stats["correct"] += int(was_correct)
356
+ reward = _latest_aggregate(store, episode)
357
+ if reward is not None:
358
+ action_stats["rewarded_episodes"] += 1
359
+ reward_totals[action_id] += reward
360
+ if len(action_stats["recent_outcomes"]) < 3:
361
+ score_breakdown = {}
362
+ for result in store.get_metric_results(episode.id, episode.agent_id):
363
+ score_breakdown[result.metric.value] = {
364
+ "normalized": result.normalized,
365
+ "status": result.status,
366
+ "reason": result.reason,
367
+ }
368
+ action_stats["recent_outcomes"].append(
369
+ {
370
+ "created_at": episode.created_at,
371
+ "was_correct": was_correct,
372
+ "reward": reward,
373
+ "execution_status": episode.execution_status,
374
+ "result_summary": episode.result_summary,
375
+ "score_breakdown": score_breakdown,
376
+ }
377
+ )
378
+ for action_id, action_stats in stats.items():
379
+ evaluated = action_stats["correctness_evaluated"]
380
+ rewarded = action_stats["rewarded_episodes"]
381
+ if evaluated:
382
+ action_stats["correctness_rate"] = action_stats["correct"] / evaluated
383
+ if rewarded:
384
+ action_stats["mean_reward"] = reward_totals[action_id] / rewarded
385
+ return {"episodes_reviewed": len(episodes), "actions": stats}
386
+
387
+
238
388
  def _policy_difference(
239
389
  current: PolicySnapshot, previous: PolicySnapshot | None
240
390
  ) -> dict[str, Any] | None:
@@ -295,6 +445,78 @@ def _cmd_show_task_policy(args: argparse.Namespace) -> int:
295
445
  return 0
296
446
 
297
447
 
448
+ def _cmd_decide_task_policy(args: argparse.Namespace) -> int:
449
+ store = get_default_store()
450
+ snapshot = store.get_active_policy(args.agent_id, args.task_id)
451
+ if snapshot is None:
452
+ print(
453
+ f"No active policy found for agent_id={args.agent_id!r}, "
454
+ f"task_id={args.task_id!r}.",
455
+ file=sys.stderr,
456
+ )
457
+ return 2
458
+ if not _is_decision_policy(snapshot):
459
+ print(
460
+ "The active policy is not marked as a delegated decision policy. "
461
+ "Questions, reporting tasks, and agent-learning automation are not eligible.",
462
+ file=sys.stderr,
463
+ )
464
+ return 2
465
+ rng = random.Random(args.seed) if args.seed is not None else None
466
+ policy = SoftmaxPolicy.from_snapshot(snapshot, rng=rng)
467
+ probabilities = policy.probabilities()
468
+ recommended_index = max(range(len(probabilities)), key=probabilities.__getitem__)
469
+ if args.greedy:
470
+ selected_index = recommended_index
471
+ selected_action = snapshot.actions[selected_index]
472
+ selected_probability = probabilities[selected_index]
473
+ logprob = math.log(max(selected_probability, 1e-12))
474
+ mode = "greedy"
475
+ else:
476
+ decision = policy.choose()
477
+ selected_action = decision.action
478
+ selected_index = next(
479
+ index
480
+ for index, action in enumerate(snapshot.actions)
481
+ if action.id == selected_action.id
482
+ )
483
+ selected_probability = probabilities[selected_index]
484
+ logprob = decision.logprob
485
+ mode = "sampled"
486
+ feedback = _decision_feedback(store, snapshot, args.history_limit)
487
+ selected_stats = feedback["actions"][selected_action.id]
488
+ recommendation = snapshot.actions[recommended_index]
489
+ print(
490
+ json.dumps(
491
+ {
492
+ "agent_id": snapshot.agent_id,
493
+ "task_id": snapshot.task_id,
494
+ "decision_context": snapshot.metadata.get("decision_context"),
495
+ "policy_id": snapshot.id,
496
+ "policy_version": snapshot.version,
497
+ "selection_mode": mode,
498
+ "selected_action": {
499
+ **selected_action.to_dict(),
500
+ "probability": selected_probability,
501
+ "logprob": logprob,
502
+ },
503
+ "recommended_action": {
504
+ **recommendation.to_dict(),
505
+ "probability": probabilities[recommended_index],
506
+ },
507
+ "action_probabilities": {
508
+ action.id: probability
509
+ for action, probability in zip(snapshot.actions, probabilities)
510
+ },
511
+ "selected_action_feedback": selected_stats,
512
+ "historical_feedback": feedback,
513
+ },
514
+ indent=2,
515
+ )
516
+ )
517
+ return 0
518
+
519
+
298
520
  def _cmd_init_task_policy(args: argparse.Namespace) -> int:
299
521
  store = get_default_store()
300
522
  if store.get_active_policy(args.agent_id, args.task_id) is not None:
@@ -310,20 +532,35 @@ def _cmd_init_task_policy(args: argparse.Namespace) -> int:
310
532
  except (OSError, json.JSONDecodeError) as exc:
311
533
  print(f"Unable to read --actions file: {exc}", file=sys.stderr)
312
534
  return 2
313
- if not isinstance(action_payloads, list) or not action_payloads:
314
- print("--actions file must contain a non-empty JSON list", file=sys.stderr)
535
+ if not isinstance(action_payloads, list) or len(action_payloads) < 2:
536
+ print(
537
+ "--actions file must contain at least two delegated decision actions",
538
+ file=sys.stderr,
539
+ )
315
540
  return 2
316
541
  try:
317
542
  actions = [Action.from_dict(item) for item in action_payloads]
318
543
  except (KeyError, TypeError, ValueError) as exc:
319
544
  print(f"Invalid action definition: {exc}", file=sys.stderr)
320
545
  return 2
546
+ action_ids = [action.id for action in actions]
547
+ if any(not action_id.strip() for action_id in action_ids) or len(set(action_ids)) != len(
548
+ action_ids
549
+ ):
550
+ print("Decision action ids must be non-empty and unique", file=sys.stderr)
551
+ return 2
321
552
  policy = SoftmaxPolicy.from_actions(
322
553
  actions,
323
554
  agent_id=args.agent_id,
324
555
  task_id=args.task_id,
325
556
  )
326
557
  snapshot = policy.snapshot()
558
+ snapshot.metadata.update(
559
+ {
560
+ "policy_scope": _DECISION_POLICY_SCOPE,
561
+ "decision_context": args.decision_context,
562
+ }
563
+ )
327
564
  store.store_policy(snapshot)
328
565
  print(json.dumps(_policy_payload(snapshot), indent=2))
329
566
  return 0
@@ -358,6 +595,26 @@ def _cmd_register_task_episode(args: argparse.Namespace) -> int:
358
595
  print(f"Invalid episode definition: {exc}", file=sys.stderr)
359
596
  return 2
360
597
 
598
+ if args.require_decision_policy:
599
+ policy = get_default_store().get_policy(episode.policy_id or "", args.agent_id)
600
+ if not _is_decision_policy(policy) or policy.task_id != args.task_id:
601
+ print(
602
+ "Episode registration requires a delegated decision policy and its policy_id.",
603
+ file=sys.stderr,
604
+ )
605
+ return 2
606
+ action_ids = {action.id for action in policy.actions}
607
+ if episode.action_id not in action_ids:
608
+ print("Episode action_id is not in the delegated decision policy.", file=sys.stderr)
609
+ return 2
610
+ correct_action_id = episode.metadata.get("correct_action_id")
611
+ if correct_action_id is not None and correct_action_id not in action_ids:
612
+ print(
613
+ "Episode metadata.correct_action_id is not in the delegated decision policy.",
614
+ file=sys.stderr,
615
+ )
616
+ return 2
617
+
361
618
  get_default_store().store_episode(episode)
362
619
  print(json.dumps(episode.to_dict(), indent=2))
363
620
  return 0
@@ -375,6 +632,7 @@ def main(argv: list[str] | None = None) -> int:
375
632
  "train": _cmd_train,
376
633
  "score": _cmd_score,
377
634
  "task-policy": _cmd_show_task_policy,
635
+ "task-policy-decide": _cmd_decide_task_policy,
378
636
  "task-policy-init": _cmd_init_task_policy,
379
637
  "task-episode-register": _cmd_register_task_episode,
380
638
  }
@@ -66,8 +66,10 @@ class LearningStore(ABC):
66
66
  *,
67
67
  task_id: Optional[str] = None,
68
68
  full_only: bool = False,
69
+ start_date: Optional[str] = None,
70
+ end_date: Optional[str] = None,
69
71
  ) -> int:
70
- """Count episodes, optionally limiting the count to full episodes."""
72
+ """Count episodes filtered by task, completeness, or time window."""
71
73
 
72
74
  # ---- Metric results -------------------------------------------
73
75
 
@@ -272,6 +272,8 @@ class CosmosStore(LearningStore):
272
272
  *,
273
273
  task_id: Optional[str] = None,
274
274
  full_only: bool = False,
275
+ start_date: Optional[str] = None,
276
+ end_date: Optional[str] = None,
275
277
  ) -> int:
276
278
  clauses = ["c.agent_id = @agent_id"]
277
279
  params: List[Dict[str, Any]] = [{"name": "@agent_id", "value": agent_id}]
@@ -281,6 +283,12 @@ class CosmosStore(LearningStore):
281
283
  else:
282
284
  clauses.append("c.task_id = @task_id")
283
285
  params.append({"name": "@task_id", "value": task_id})
286
+ if start_date:
287
+ clauses.append("c.created_at >= @start_date")
288
+ params.append({"name": "@start_date", "value": start_date})
289
+ if end_date:
290
+ clauses.append("c.created_at <= @end_date")
291
+ params.append({"name": "@end_date", "value": end_date})
284
292
  if full_only:
285
293
  clauses.extend(
286
294
  [
@@ -212,12 +212,16 @@ class LocalFileStore(LearningStore):
212
212
  *,
213
213
  task_id: Optional[str] = None,
214
214
  full_only: bool = False,
215
+ start_date: Optional[str] = None,
216
+ end_date: Optional[str] = None,
215
217
  ) -> int:
216
218
  return sum(
217
219
  1
218
220
  for doc in self._read_dir_docs("episodes", agent_id)
219
221
  if (task_id is None or doc.get("task_id", "default") == task_id)
220
222
  and (not full_only or Episode.from_dict(doc).is_full)
223
+ and (start_date is None or doc.get("created_at", "") >= start_date)
224
+ and (end_date is None or doc.get("created_at", "") <= end_date)
221
225
  )
222
226
 
223
227
  # ------------------------------------------------------------------
@@ -93,6 +93,8 @@ class InMemoryStore(LearningStore):
93
93
  *,
94
94
  task_id: Optional[str] = None,
95
95
  full_only: bool = False,
96
+ start_date: Optional[str] = None,
97
+ end_date: Optional[str] = None,
96
98
  ) -> int:
97
99
  return sum(
98
100
  1
@@ -100,6 +102,8 @@ class InMemoryStore(LearningStore):
100
102
  if episode.agent_id == agent_id
101
103
  and (task_id is None or episode.task_id == task_id)
102
104
  and (not full_only or episode.is_full)
105
+ and (start_date is None or episode.created_at >= start_date)
106
+ and (end_date is None or episode.created_at <= end_date)
103
107
  )
104
108
 
105
109
  # ---- Metric results -------------------------------------------