agent-learning 0.4.3__tar.gz → 0.5.1__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 (96) hide show
  1. {agent_learning-0.4.3/src/agent_learning.egg-info → agent_learning-0.5.1}/PKG-INFO +9 -1
  2. {agent_learning-0.4.3 → agent_learning-0.5.1}/PYPI.md +8 -0
  3. agent_learning-0.5.1/README.md +188 -0
  4. {agent_learning-0.4.3 → agent_learning-0.5.1}/pyproject.toml +1 -1
  5. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/_version.py +1 -1
  6. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/cli.py +240 -7
  7. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/storage/base.py +2 -1
  8. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/storage/cosmos.py +18 -2
  9. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/storage/local.py +2 -0
  10. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/storage/memory.py +2 -0
  11. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/training/runner.py +1 -0
  12. {agent_learning-0.4.3 → agent_learning-0.5.1/src/agent_learning.egg-info}/PKG-INFO +9 -1
  13. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_cli.py +383 -2
  14. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_end_to_end.py +28 -2
  15. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_storage_local.py +9 -1
  16. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_storage_memory.py +9 -1
  17. agent_learning-0.4.3/README.md +0 -82
  18. {agent_learning-0.4.3 → agent_learning-0.5.1}/LICENSE +0 -0
  19. {agent_learning-0.4.3 → agent_learning-0.5.1}/setup.cfg +0 -0
  20. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/__init__.py +0 -0
  21. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/capture.py +0 -0
  22. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/__init__.py +0 -0
  23. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/base.py +0 -0
  24. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/router.py +0 -0
  25. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/scorers/__init__.py +0 -0
  26. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/scorers/_base.py +0 -0
  27. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/scorers/adherence.py +0 -0
  28. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/scorers/completion.py +0 -0
  29. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/classifiers/scorers/intent.py +0 -0
  30. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/config.py +0 -0
  31. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/learners/__init__.py +0 -0
  32. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/learners/base.py +0 -0
  33. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/learners/reinforce.py +0 -0
  34. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/__init__.py +0 -0
  35. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/base.py +0 -0
  36. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/intent_resolution.py +0 -0
  37. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/local.py +0 -0
  38. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/registry.py +0 -0
  39. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/task_adherence.py +0 -0
  40. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/metrics/task_completion.py +0 -0
  41. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/policy/__init__.py +0 -0
  42. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/policy/base.py +0 -0
  43. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/policy/contextual_softmax.py +0 -0
  44. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/policy/softmax_bandit.py +0 -0
  45. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/py.typed +0 -0
  46. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/rewards/__init__.py +0 -0
  47. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/rewards/shaping.py +0 -0
  48. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/rewards/writer.py +0 -0
  49. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/__init__.py +0 -0
  50. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/base.py +0 -0
  51. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/llm/__init__.py +0 -0
  52. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/llm/_base.py +0 -0
  53. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/llm/adherence.py +0 -0
  54. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/llm/completion.py +0 -0
  55. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/llm/intent.py +0 -0
  56. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp/__init__.py +0 -0
  57. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp/_base.py +0 -0
  58. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp/adherence.py +0 -0
  59. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp/completion.py +0 -0
  60. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp/intent.py +0 -0
  61. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp_text/__init__.py +0 -0
  62. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp_text/_base.py +0 -0
  63. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp_text/adherence.py +0 -0
  64. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp_text/completion.py +0 -0
  65. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/nlp_text/intent.py +0 -0
  66. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/slm/__init__.py +0 -0
  67. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/slm/_base.py +0 -0
  68. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/slm/adherence.py +0 -0
  69. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/slm/completion.py +0 -0
  70. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/slm/intent.py +0 -0
  71. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/stdlib/__init__.py +0 -0
  72. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/stdlib/_text.py +0 -0
  73. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/stdlib/adherence.py +0 -0
  74. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/stdlib/completion.py +0 -0
  75. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/scorers/stdlib/intent.py +0 -0
  76. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/storage/__init__.py +0 -0
  77. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/training/__init__.py +0 -0
  78. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning/types.py +0 -0
  79. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning.egg-info/SOURCES.txt +0 -0
  80. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning.egg-info/dependency_links.txt +0 -0
  81. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning.egg-info/entry_points.txt +0 -0
  82. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning.egg-info/requires.txt +0 -0
  83. {agent_learning-0.4.3 → agent_learning-0.5.1}/src/agent_learning.egg-info/top_level.txt +0 -0
  84. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_capture.py +0 -0
  85. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_contextual_policy.py +0 -0
  86. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_default_store.py +0 -0
  87. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_learner.py +0 -0
  88. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_metrics_local.py +0 -0
  89. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_policy.py +0 -0
  90. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_scorers.py +0 -0
  91. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_scorers_llm.py +0 -0
  92. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_scorers_nlp_text.py +0 -0
  93. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_scorers_slm.py +0 -0
  94. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_scorers_stdlib.py +0 -0
  95. {agent_learning-0.4.3 → agent_learning-0.5.1}/tests/test_shaping.py +0 -0
  96. {agent_learning-0.4.3 → agent_learning-0.5.1}/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.3
3
+ Version: 0.5.1
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
@@ -69,6 +69,10 @@ Dynamic: license-file
69
69
 
70
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
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.
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:
@@ -79,4 +83,8 @@ The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tu
79
83
 
80
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.
81
85
 
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.
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.
@@ -2,6 +2,10 @@
2
2
 
3
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
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
+
5
9
  ## How it works
6
10
 
7
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,4 +16,8 @@ The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tu
12
16
 
13
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.
14
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
+
15
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.
@@ -0,0 +1,188 @@
1
+ # agent-learning
2
+
3
+ Helping agents make better decisions with measurable feedback.
4
+
5
+ `agent-learning` is a lightweight decision layer for an existing agent. It
6
+ learns which option works best for a recurring task from a small, explicit set
7
+ of executable alternatives, such as prompt variants, retrieval depths, tools,
8
+ models, workflows, or escalation paths.
9
+
10
+ The foundation model stays frozen. There are no GPU training jobs and no
11
+ hidden prompt rewrites. The agent makes an explicit choice, records the
12
+ user-visible outcome, scores the evidence, and applies a small CPU update to
13
+ the next decision.
14
+
15
+ <p align="center">
16
+ <img src="images/agent-decision-making.svg" alt="Animated agentic decision loop: choose a bounded action, execute it, score the observed outcome, and improve the next decision" width="960" style="max-width:100%; height:auto;" />
17
+ </p>
18
+
19
+ ## The decision loop
20
+
21
+ 1. **Frame a reusable decision.** Define a stable decision context and at
22
+ least two actions the agent can actually execute. A TaskPolicy owns the
23
+ probability distribution for that `(agent_id, task_id)` pair.
24
+
25
+ 2. **Choose and execute.** `task-policy-decide` samples an action from the
26
+ active softmax policy and returns learned feedback from earlier attempts.
27
+ The agent uses that feedback and executes the selected action.
28
+
29
+ 3. **Observe and score.** A completed episode preserves the decision context,
30
+ selected action, output, result summary, latency, and independently
31
+ supported correctness evidence. Local scorers measure intent resolution,
32
+ task adherence, and task completion and shape them into one reward.
33
+
34
+ 4. **Improve the next decision.** REINFORCE-with-baseline nudges a few policy
35
+ logits, then persists a new policy snapshot. Better-than-usual choices gain
36
+ probability, worse-than-usual choices lose probability, and exploration
37
+ remains available.
38
+
39
+ Everything runs in the existing Python process. Scoring is local by default;
40
+ configured Azure AI evaluators remain available as an opt-in. Stores can be
41
+ in-memory, local JSON files, or Azure Cosmos DB.
42
+
43
+ ## What counts as a decision
44
+
45
+ A TaskPolicy is appropriate only when all of these are true:
46
+
47
+ - The agent must choose among at least two explicit executable alternatives.
48
+ - The alternatives are stable enough to reuse on future requests.
49
+ - The choice can affect quality, correctness, latency, cost, safety, or
50
+ completion.
51
+ - An observable outcome can later score whether the choice was useful.
52
+
53
+ Good examples include choosing a retrieval strategy, model, tool, workflow,
54
+ Azure workload, or escalation path for a concrete use case. Factual questions,
55
+ ordinary chat, summaries, reporting, and the learning automation itself are not
56
+ decision policies. Repetition without an executed outcome is not feedback.
57
+
58
+ Before every eligible execution, the decision response includes the selected
59
+ and currently recommended actions, policy version and probability, correctness
60
+ rate, mean reward, metric scores, and recent result summaries. This makes the
61
+ learned evidence useful during execution instead of producing a policy that is
62
+ never consumed.
63
+
64
+ ## Install
65
+
66
+ ### Windows CLI
67
+
68
+ For a Python-independent installation, download `agent-learn.exe` or the
69
+ standalone installer from the
70
+ [latest GitHub release](https://github.com/microsoft/agent-learning/releases/latest).
71
+ The installer can add its installation directory to your user `PATH`, so
72
+ `agent-learn` works from PowerShell or Command Prompt without Python or `pip`.
73
+
74
+ ```powershell
75
+ agent-learn.exe --help
76
+ ```
77
+
78
+ ### Python SDK
79
+
80
+ Released versions are published to PyPI:
81
+ <https://pypi.org/project/agent-learning/>.
82
+
83
+ ```shell
84
+ pip install agent-learning
85
+ ```
86
+
87
+ ## Quickstart: improve one recurring decision
88
+
89
+ Use one durable store across CLI processes:
90
+
91
+ ```powershell
92
+ $env:AGENT_LEARNING_STORE_BACKEND = "local"
93
+ $env:AGENT_LEARNING_LOCAL_STORE_DIR = Join-Path $env:LOCALAPPDATA "agent-learning\store"
94
+ ```
95
+
96
+ Define actions the agent can really execute in `actions.json`:
97
+
98
+ ```json
99
+ [
100
+ {
101
+ "id": "use_text_search",
102
+ "description": "Search for exact symbols and terms",
103
+ "parameters": {"strategy": "text"}
104
+ },
105
+ {
106
+ "id": "use_semantic_search",
107
+ "description": "Search the repository by meaning",
108
+ "parameters": {"strategy": "semantic"}
109
+ }
110
+ ]
111
+ ```
112
+
113
+ Initialize the decision once, then ask the active policy what to execute:
114
+
115
+ ```powershell
116
+ agent-learn task-policy-init `
117
+ --agent-id code-reviewer `
118
+ --task-id choose-repository-search `
119
+ --decision-context "Choose a repository search strategy for a coding task" `
120
+ --actions .\actions.json
121
+
122
+ agent-learn task-policy-decide `
123
+ --agent-id code-reviewer `
124
+ --task-id choose-repository-search
125
+ ```
126
+
127
+ Execute the returned `selected_action`, preserve its policy fields, and record
128
+ the independently observed outcome in `episode.json`. Then close the loop:
129
+
130
+ ```powershell
131
+ agent-learn task-episode-register `
132
+ --agent-id code-reviewer `
133
+ --task-id choose-repository-search `
134
+ --episode .\episode.json `
135
+ --require-decision-policy
136
+
137
+ agent-learn score --agent-id code-reviewer --task-id choose-repository-search
138
+ agent-learn train --agent-id code-reviewer --task-id choose-repository-search --decision-only
139
+ agent-learn task-policy-decide --agent-id code-reviewer --task-id choose-repository-search
140
+ ```
141
+
142
+ The final decision consumes the updated probabilities and feedback from prior
143
+ executions. Training defaults to a minimum of five completed episodes. See the
144
+ [decision-making guide](docs/decision-making.md) for the episode schema,
145
+ evidence rules, math, and deployment patterns.
146
+
147
+ ## Functional Testing
148
+
149
+ A good way to see the SDK in action is to run the
150
+ interactive capture scenario first, followed by the offline batch update:
151
+
152
+ ```powershell
153
+ python tests/functional_cli_interactive.py
154
+ python tests/functional_cli_batch.py
155
+ ```
156
+
157
+ ## Usage
158
+
159
+ The `agent-learn` CLI provides the decision lifecycle and inspection
160
+ operations:
161
+
162
+ ```text
163
+ agent-learn list
164
+ agent-learn --version
165
+ agent-learn tasks-list <agent_id> [--decision-only]
166
+ agent-learn task-episodes-count <agent_id> [--task-id <task_id>] [--include-incomplete] [--start-date <date>] [--end-date <date>]
167
+ agent-learn task-episodes-list <agent_id> [--task-id <task_id>] [--limit <1-500>] [--include-incomplete] [--start-date <date>] [--end-date <date>]
168
+ agent-learn task-policy-init --agent-id <agent_id> --task-id <task_id> --decision-context <context> --actions ./actions.json
169
+ agent-learn task-policy-decide --agent-id <agent_id> --task-id <task_id> [--history-limit <1-500>] [--greedy] [--seed <integer>]
170
+ agent-learn task-episode-register --agent-id <agent_id> --task-id <task_id> --episode ./episode.json [--require-decision-policy]
171
+ agent-learn score --agent-id <agent_id> [--task-id <task_id>] [--limit <1-500>]
172
+ 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]
173
+ agent-learn task-policy --agent-id <agent_id> --task-id <task_id>
174
+ ```
175
+
176
+ ## Documentation
177
+
178
+ - [Agentic decision making](docs/decision-making.md): concepts, evidence,
179
+ workflow, math, and deployment.
180
+ - [Scout decision integration](docs/scout-agent-learn-skill.md): apply learned
181
+ delegated decisions during execution.
182
+ - [Scout decision training](docs/scout-automation-agent-learning.md): train
183
+ eligible policies without turning automation into a policy task.
184
+ - [The math, explained simply](docs/math-explained-simply.md) and
185
+ [mathematical reference](docs/math.md): softmax, rewards, baselines, and
186
+ REINFORCE.
187
+ - [Tiered scoring design](docs/design.md): local and Azure-backed outcome
188
+ scoring.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-learning"
7
- version = "0.4.3"
7
+ version = "0.5.1"
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.3"
3
+ __version__ = "0.5.1"
@@ -5,12 +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
11
13
  from datetime import datetime, timezone
12
14
  from typing import Any
13
15
 
16
+ from ._version import __version__
14
17
  from .policy.softmax_bandit import SoftmaxPolicy
15
18
  from .storage.cosmos import get_default_store
16
19
  from .training.runner import LearningRunner
@@ -19,6 +22,7 @@ from .types import Action, Episode, MetricName, PolicySnapshot, RewardSource
19
22
  logger = logging.getLogger(__name__)
20
23
 
21
24
  _MAX_EPISODES = 500
25
+ _DECISION_POLICY_SCOPE = "delegated_decision"
22
26
 
23
27
 
24
28
  def _episode_limit(value: str) -> int:
@@ -39,13 +43,18 @@ def _iso_date(value: str) -> str:
39
43
 
40
44
 
41
45
  def _build_arg_parser() -> argparse.ArgumentParser:
42
- 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__}")
43
51
  sub = parser.add_subparsers(dest="command", required=True)
44
52
 
45
53
  sub.add_parser("list", help="List discovered agent ids and names.")
46
54
 
47
55
  tasks = sub.add_parser("tasks-list", help="List tasks for an agent.")
48
56
  tasks.add_argument("agent_id")
57
+ tasks.add_argument("--decision-only", action="store_true")
49
58
 
50
59
  count = sub.add_parser(
51
60
  "task-episodes-count",
@@ -53,6 +62,11 @@ def _build_arg_parser() -> argparse.ArgumentParser:
53
62
  )
54
63
  count.add_argument("agent_id")
55
64
  count.add_argument("--task-id")
65
+ count.add_argument(
66
+ "--include-incomplete",
67
+ action="store_true",
68
+ help="Count pending and incomplete attempts as well as full learning episodes.",
69
+ )
56
70
  count.add_argument("--start-date", type=_iso_date)
57
71
  count.add_argument("--end-date", type=_iso_date)
58
72
 
@@ -72,6 +86,7 @@ def _build_arg_parser() -> argparse.ArgumentParser:
72
86
  train.add_argument("--task-id")
73
87
  train.add_argument("--limit", type=_episode_limit, default=200)
74
88
  train.add_argument("--min-episodes", type=_episode_limit, default=1)
89
+ train.add_argument("--decision-only", action="store_true")
75
90
  train.add_argument("--start-date", type=_iso_date)
76
91
  train.add_argument("--end-date", type=_iso_date)
77
92
  train.add_argument(
@@ -89,12 +104,27 @@ def _build_arg_parser() -> argparse.ArgumentParser:
89
104
  show.add_argument("--agent-id", required=True)
90
105
  show.add_argument("--task-id", required=True)
91
106
 
107
+ decide = sub.add_parser(
108
+ "task-policy-decide",
109
+ help="Choose a delegated decision action and return learned feedback.",
110
+ )
111
+ decide.add_argument("--agent-id", required=True)
112
+ decide.add_argument("--task-id", required=True)
113
+ decide.add_argument("--history-limit", type=_episode_limit, default=100)
114
+ decide.add_argument("--greedy", action="store_true")
115
+ decide.add_argument("--seed", type=int)
116
+
92
117
  init = sub.add_parser(
93
118
  "task-policy-init",
94
119
  help="Create and activate the initial policy for an agent task.",
95
120
  )
96
121
  init.add_argument("--agent-id", required=True)
97
122
  init.add_argument("--task-id", required=True)
123
+ init.add_argument(
124
+ "--decision-context",
125
+ required=True,
126
+ help="Stable description of the delegated choice this policy controls.",
127
+ )
98
128
  init.add_argument(
99
129
  "--actions",
100
130
  required=True,
@@ -107,6 +137,7 @@ def _build_arg_parser() -> argparse.ArgumentParser:
107
137
  )
108
138
  register.add_argument("--agent-id", required=True)
109
139
  register.add_argument("--task-id", required=True)
140
+ register.add_argument("--require-decision-policy", action="store_true")
110
141
  register.add_argument(
111
142
  "--episode",
112
143
  required=True,
@@ -124,7 +155,14 @@ def _cmd_agents_list(args: argparse.Namespace) -> int:
124
155
 
125
156
 
126
157
  def _cmd_agent_tasks_list(args: argparse.Namespace) -> int:
127
- tasks = get_default_store().list_agent_tasks(args.agent_id)
158
+ store = get_default_store()
159
+ tasks = store.list_agent_tasks(args.agent_id)
160
+ if args.decision_only:
161
+ tasks = [
162
+ task
163
+ for task in tasks
164
+ if _is_decision_policy(store.get_active_policy(args.agent_id, task.id))
165
+ ]
128
166
  print(json.dumps([{"id": task.id, "name": task.name} for task in tasks], indent=2))
129
167
  return 0
130
168
 
@@ -133,7 +171,7 @@ def _cmd_agents_episodes_count(args: argparse.Namespace) -> int:
133
171
  count = get_default_store().count_episodes(
134
172
  args.agent_id,
135
173
  task_id=args.task_id,
136
- full_only=True,
174
+ full_only=not args.include_incomplete,
137
175
  start_date=args.start_date,
138
176
  end_date=args.end_date,
139
177
  )
@@ -149,9 +187,8 @@ def _cmd_agents_episodes_list(args: argparse.Namespace) -> int:
149
187
  limit=_MAX_EPISODES,
150
188
  start_date=args.start_date,
151
189
  end_date=args.end_date,
190
+ full_only=not args.include_incomplete,
152
191
  )
153
- if not args.include_incomplete:
154
- episodes = [episode for episode in episodes if episode.is_full]
155
192
 
156
193
  payload = []
157
194
  for episode in episodes[: args.limit]:
@@ -190,6 +227,7 @@ def _cmd_train(args: argparse.Namespace) -> int:
190
227
  limit=args.limit,
191
228
  start_date=args.start_date,
192
229
  end_date=args.end_date,
230
+ full_only=True,
193
231
  )
194
232
  episode_limits = Counter(episode.task_id for episode in selected_episodes)
195
233
  runs: list[dict[str, Any]] = []
@@ -199,6 +237,9 @@ def _cmd_train(args: argparse.Namespace) -> int:
199
237
  if snapshot is None:
200
238
  skipped.append({"task_id": task_id, "reason": "no active policy"})
201
239
  continue
240
+ if args.decision_only and not _is_decision_policy(snapshot):
241
+ skipped.append({"task_id": task_id, "reason": "not a delegated decision policy"})
242
+ continue
202
243
  episode_limit = episode_limits.get(task_id, 0)
203
244
  if episode_limit == 0:
204
245
  skipped.append({"task_id": task_id, "reason": "no episodes in selected batch"})
@@ -244,6 +285,7 @@ def _cmd_score(args: argparse.Namespace) -> int:
244
285
  args.agent_id,
245
286
  task_id=args.task_id,
246
287
  limit=args.limit,
288
+ full_only=True,
247
289
  )
248
290
  scored = 0
249
291
  for episode in episodes:
@@ -266,6 +308,89 @@ def _policy_payload(snapshot: PolicySnapshot) -> dict[str, Any]:
266
308
  return payload
267
309
 
268
310
 
311
+ def _is_decision_policy(snapshot: PolicySnapshot | None) -> bool:
312
+ return bool(
313
+ snapshot
314
+ and snapshot.metadata.get("policy_scope") == _DECISION_POLICY_SCOPE
315
+ )
316
+
317
+
318
+ def _latest_aggregate(store: Any, episode: Episode) -> float | None:
319
+ rewards = [
320
+ reward
321
+ for reward in store.get_rewards_for_episode(episode.id, episode.agent_id)
322
+ if reward.source == RewardSource.AGGREGATE
323
+ ]
324
+ if not rewards:
325
+ return None
326
+ return max(rewards, key=lambda reward: reward.created_at).value
327
+
328
+
329
+ def _decision_feedback(
330
+ store: Any, snapshot: PolicySnapshot, history_limit: int
331
+ ) -> dict[str, Any]:
332
+ stats = {
333
+ action.id: {
334
+ "attempts": 0,
335
+ "correctness_evaluated": 0,
336
+ "correct": 0,
337
+ "correctness_rate": None,
338
+ "rewarded_episodes": 0,
339
+ "mean_reward": None,
340
+ "recent_outcomes": [],
341
+ }
342
+ for action in snapshot.actions
343
+ }
344
+ reward_totals = {action.id: 0.0 for action in snapshot.actions}
345
+ episodes = store.query_episodes(
346
+ snapshot.agent_id,
347
+ task_id=snapshot.task_id,
348
+ limit=history_limit,
349
+ )
350
+ for episode in episodes:
351
+ action_id = episode.action_id
352
+ if action_id not in stats:
353
+ continue
354
+ action_stats = stats[action_id]
355
+ action_stats["attempts"] += 1
356
+ correct_action_id = episode.metadata.get("correct_action_id")
357
+ was_correct = None
358
+ if correct_action_id:
359
+ was_correct = action_id == correct_action_id
360
+ action_stats["correctness_evaluated"] += 1
361
+ action_stats["correct"] += int(was_correct)
362
+ reward = _latest_aggregate(store, episode)
363
+ if reward is not None:
364
+ action_stats["rewarded_episodes"] += 1
365
+ reward_totals[action_id] += reward
366
+ if len(action_stats["recent_outcomes"]) < 3:
367
+ score_breakdown = {}
368
+ for result in store.get_metric_results(episode.id, episode.agent_id):
369
+ score_breakdown[result.metric.value] = {
370
+ "normalized": result.normalized,
371
+ "status": result.status,
372
+ "reason": result.reason,
373
+ }
374
+ action_stats["recent_outcomes"].append(
375
+ {
376
+ "created_at": episode.created_at,
377
+ "was_correct": was_correct,
378
+ "reward": reward,
379
+ "execution_status": episode.execution_status,
380
+ "result_summary": episode.result_summary,
381
+ "score_breakdown": score_breakdown,
382
+ }
383
+ )
384
+ for action_id, action_stats in stats.items():
385
+ evaluated = action_stats["correctness_evaluated"]
386
+ rewarded = action_stats["rewarded_episodes"]
387
+ if evaluated:
388
+ action_stats["correctness_rate"] = action_stats["correct"] / evaluated
389
+ if rewarded:
390
+ action_stats["mean_reward"] = reward_totals[action_id] / rewarded
391
+ return {"episodes_reviewed": len(episodes), "actions": stats}
392
+
393
+
269
394
  def _policy_difference(
270
395
  current: PolicySnapshot, previous: PolicySnapshot | None
271
396
  ) -> dict[str, Any] | None:
@@ -326,6 +451,78 @@ def _cmd_show_task_policy(args: argparse.Namespace) -> int:
326
451
  return 0
327
452
 
328
453
 
454
+ def _cmd_decide_task_policy(args: argparse.Namespace) -> int:
455
+ store = get_default_store()
456
+ snapshot = store.get_active_policy(args.agent_id, args.task_id)
457
+ if snapshot is None:
458
+ print(
459
+ f"No active policy found for agent_id={args.agent_id!r}, "
460
+ f"task_id={args.task_id!r}.",
461
+ file=sys.stderr,
462
+ )
463
+ return 2
464
+ if not _is_decision_policy(snapshot):
465
+ print(
466
+ "The active policy is not marked as a delegated decision policy. "
467
+ "Questions, reporting tasks, and agent-learning automation are not eligible.",
468
+ file=sys.stderr,
469
+ )
470
+ return 2
471
+ rng = random.Random(args.seed) if args.seed is not None else None
472
+ policy = SoftmaxPolicy.from_snapshot(snapshot, rng=rng)
473
+ probabilities = policy.probabilities()
474
+ recommended_index = max(range(len(probabilities)), key=probabilities.__getitem__)
475
+ if args.greedy:
476
+ selected_index = recommended_index
477
+ selected_action = snapshot.actions[selected_index]
478
+ selected_probability = probabilities[selected_index]
479
+ logprob = math.log(max(selected_probability, 1e-12))
480
+ mode = "greedy"
481
+ else:
482
+ decision = policy.choose()
483
+ selected_action = decision.action
484
+ selected_index = next(
485
+ index
486
+ for index, action in enumerate(snapshot.actions)
487
+ if action.id == selected_action.id
488
+ )
489
+ selected_probability = probabilities[selected_index]
490
+ logprob = decision.logprob
491
+ mode = "sampled"
492
+ feedback = _decision_feedback(store, snapshot, args.history_limit)
493
+ selected_stats = feedback["actions"][selected_action.id]
494
+ recommendation = snapshot.actions[recommended_index]
495
+ print(
496
+ json.dumps(
497
+ {
498
+ "agent_id": snapshot.agent_id,
499
+ "task_id": snapshot.task_id,
500
+ "decision_context": snapshot.metadata.get("decision_context"),
501
+ "policy_id": snapshot.id,
502
+ "policy_version": snapshot.version,
503
+ "selection_mode": mode,
504
+ "selected_action": {
505
+ **selected_action.to_dict(),
506
+ "probability": selected_probability,
507
+ "logprob": logprob,
508
+ },
509
+ "recommended_action": {
510
+ **recommendation.to_dict(),
511
+ "probability": probabilities[recommended_index],
512
+ },
513
+ "action_probabilities": {
514
+ action.id: probability
515
+ for action, probability in zip(snapshot.actions, probabilities)
516
+ },
517
+ "selected_action_feedback": selected_stats,
518
+ "historical_feedback": feedback,
519
+ },
520
+ indent=2,
521
+ )
522
+ )
523
+ return 0
524
+
525
+
329
526
  def _cmd_init_task_policy(args: argparse.Namespace) -> int:
330
527
  store = get_default_store()
331
528
  if store.get_active_policy(args.agent_id, args.task_id) is not None:
@@ -341,20 +538,35 @@ def _cmd_init_task_policy(args: argparse.Namespace) -> int:
341
538
  except (OSError, json.JSONDecodeError) as exc:
342
539
  print(f"Unable to read --actions file: {exc}", file=sys.stderr)
343
540
  return 2
344
- if not isinstance(action_payloads, list) or not action_payloads:
345
- print("--actions file must contain a non-empty JSON list", file=sys.stderr)
541
+ if not isinstance(action_payloads, list) or len(action_payloads) < 2:
542
+ print(
543
+ "--actions file must contain at least two delegated decision actions",
544
+ file=sys.stderr,
545
+ )
346
546
  return 2
347
547
  try:
348
548
  actions = [Action.from_dict(item) for item in action_payloads]
349
549
  except (KeyError, TypeError, ValueError) as exc:
350
550
  print(f"Invalid action definition: {exc}", file=sys.stderr)
351
551
  return 2
552
+ action_ids = [action.id for action in actions]
553
+ if any(not action_id.strip() for action_id in action_ids) or len(set(action_ids)) != len(
554
+ action_ids
555
+ ):
556
+ print("Decision action ids must be non-empty and unique", file=sys.stderr)
557
+ return 2
352
558
  policy = SoftmaxPolicy.from_actions(
353
559
  actions,
354
560
  agent_id=args.agent_id,
355
561
  task_id=args.task_id,
356
562
  )
357
563
  snapshot = policy.snapshot()
564
+ snapshot.metadata.update(
565
+ {
566
+ "policy_scope": _DECISION_POLICY_SCOPE,
567
+ "decision_context": args.decision_context,
568
+ }
569
+ )
358
570
  store.store_policy(snapshot)
359
571
  print(json.dumps(_policy_payload(snapshot), indent=2))
360
572
  return 0
@@ -389,6 +601,26 @@ def _cmd_register_task_episode(args: argparse.Namespace) -> int:
389
601
  print(f"Invalid episode definition: {exc}", file=sys.stderr)
390
602
  return 2
391
603
 
604
+ if args.require_decision_policy:
605
+ policy = get_default_store().get_policy(episode.policy_id or "", args.agent_id)
606
+ if not _is_decision_policy(policy) or policy.task_id != args.task_id:
607
+ print(
608
+ "Episode registration requires a delegated decision policy and its policy_id.",
609
+ file=sys.stderr,
610
+ )
611
+ return 2
612
+ action_ids = {action.id for action in policy.actions}
613
+ if episode.action_id not in action_ids:
614
+ print("Episode action_id is not in the delegated decision policy.", file=sys.stderr)
615
+ return 2
616
+ correct_action_id = episode.metadata.get("correct_action_id")
617
+ if correct_action_id is not None and correct_action_id not in action_ids:
618
+ print(
619
+ "Episode metadata.correct_action_id is not in the delegated decision policy.",
620
+ file=sys.stderr,
621
+ )
622
+ return 2
623
+
392
624
  get_default_store().store_episode(episode)
393
625
  print(json.dumps(episode.to_dict(), indent=2))
394
626
  return 0
@@ -406,6 +638,7 @@ def main(argv: list[str] | None = None) -> int:
406
638
  "train": _cmd_train,
407
639
  "score": _cmd_score,
408
640
  "task-policy": _cmd_show_task_policy,
641
+ "task-policy-decide": _cmd_decide_task_policy,
409
642
  "task-policy-init": _cmd_init_task_policy,
410
643
  "task-episode-register": _cmd_register_task_episode,
411
644
  }
@@ -56,8 +56,9 @@ class LearningStore(ABC):
56
56
  end_date: Optional[str] = None,
57
57
  policy_id: Optional[str] = None,
58
58
  task_id: Optional[str] = None,
59
+ full_only: bool = False,
59
60
  ) -> List[Episode]:
60
- """List episodes filtered by optional time window, policy, or task."""
61
+ """List episodes filtered by time, policy, task, or completeness."""
61
62
 
62
63
  @abstractmethod
63
64
  def count_episodes(