langchain-task-steering 0.1.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.
@@ -0,0 +1,27 @@
1
+ name: Publish to Pypi
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Build package
22
+ run: |
23
+ pip install build
24
+ python -m build
25
+
26
+ - name: Publish to Pypi
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ *.egg
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+ env/
14
+
15
+ # Testing
16
+ .pytest_cache/
17
+ .coverage
18
+ htmlcov/
19
+
20
+ # IDE
21
+ .idea/
22
+ .vscode/
23
+ *.swp
24
+ *.swo
25
+
26
+ # OS
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Archives
31
+ *.zip
32
+
33
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 task-steering contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,310 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-task-steering
3
+ Version: 0.1.0
4
+ Summary: Implicit state machine middleware for LangChain v1 agents. Ordered task pipelines with per-task tool scoping, prompt injection, and composable validation.
5
+ Project-URL: Homepage, https://github.com/edvinhallvaxhiu/langchain-task-steering
6
+ Project-URL: Repository, https://github.com/edvinhallvaxhiu/langchain-task-steering
7
+ Project-URL: Issues, https://github.com/edvinhallvaxhiu/langchain-task-steering/issues
8
+ Author: Edvin Hallvaxhiu
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,langchain,langgraph,middleware,state-machine,task-pipeline
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: langchain>=1.0.0
22
+ Requires-Dist: langgraph>=0.4.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-cov; extra == 'dev'
25
+ Requires-Dist: pytest>=8.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # task-steering
29
+
30
+ Implicit state-machine middleware for [LangChain v1](https://python.langchain.com/) agents. Define ordered task pipelines with per-task tool scoping, dynamic prompt injection, and composable validation — all as a drop-in `AgentMiddleware`.
31
+
32
+ ```
33
+ PENDING ──> IN_PROGRESS ──> COMPLETE
34
+ ```
35
+
36
+ The model drives its own transitions by calling `update_task_status`. The middleware enforces ordering, scopes tools, injects the active task's instruction into the system prompt, and gates completion via pluggable validators.
37
+
38
+ ## When to use this
39
+
40
+ | Scenario | task-steering | LangGraph explicit workflows |
41
+ |---|:---:|:---:|
42
+ | Linear task pipeline (A then B then C) | **Best fit** | Verbose — one node + edges per task |
43
+ | Per-task tool scoping | **Built-in** | Manual — separate tool lists per node |
44
+ | Dynamic tasks from config / DB | **Easy** — tasks are data | Hard — graph is compiled at build time |
45
+ | Branching / parallel execution | Not supported | **Built-in** — edges + `Send()` |
46
+ | Per-task human-in-the-loop interrupts | Not supported | **Built-in** — `interrupt()` per node |
47
+ | Complex orchestration with retries / cycles | Not supported | **Built-in** — conditional edges |
48
+ | Composition with other middleware | **Native** — it's an `AgentMiddleware` | N/A — different abstraction |
49
+ | Debuggability in LangGraph Studio | Opaque — single agent node | **Clear** — each node visible in traces |
50
+
51
+ **Rule of thumb:** If your tasks are sequential and tool-scoped, use task-steering. If you need branching, parallelism, or per-task interrupts, use explicit LangGraph workflows.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install task-steering
57
+ ```
58
+
59
+ For development:
60
+
61
+ ```bash
62
+ git clone https://github.com/edvinhallvaxhiu/task-steering
63
+ cd task-steering
64
+ pip install -e ".[dev]"
65
+ ```
66
+
67
+ ### Requirements
68
+
69
+ - Python >= 3.10
70
+ - `langchain >= 1.0.0`
71
+ - `langgraph >= 0.4.0`
72
+
73
+ ## Quick start
74
+
75
+ ```python
76
+ from langchain.agents import create_agent
77
+ from langchain.tools import tool
78
+ from task_steering import TaskSteeringMiddleware, Task
79
+
80
+
81
+ @tool
82
+ def add_items(items: list[str]) -> str:
83
+ """Add items to the inventory."""
84
+ return f"Added {len(items)} items."
85
+
86
+
87
+ @tool
88
+ def categorize(categories: dict[str, list[str]]) -> str:
89
+ """Assign items to categories."""
90
+ return f"Categorized into {len(categories)} groups."
91
+
92
+
93
+ pipeline = TaskSteeringMiddleware(
94
+ tasks=[
95
+ Task(
96
+ name="collect",
97
+ instruction="Collect all relevant items from the user's input.",
98
+ tools=[add_items],
99
+ ),
100
+ Task(
101
+ name="categorize",
102
+ instruction="Organize the collected items into categories.",
103
+ tools=[categorize],
104
+ ),
105
+ ],
106
+ )
107
+
108
+ agent = create_agent(
109
+ model="anthropic:claude-sonnet-4-6",
110
+ middleware=[pipeline],
111
+ system_prompt="You are an inventory assistant.",
112
+ )
113
+
114
+ result = agent.invoke(
115
+ {"messages": [{"role": "user", "content": "I have apples, bolts, and milk."}]}
116
+ )
117
+ ```
118
+
119
+ The agent automatically receives an `update_task_status` tool and sees a task pipeline block in its system prompt. It must complete `collect` before starting `categorize`.
120
+
121
+ ## How it works
122
+
123
+ ### What the model sees
124
+
125
+ Every model call, the middleware appends a status block to the system prompt:
126
+
127
+ ```xml
128
+ <task_pipeline>
129
+ [x] collect (complete)
130
+ [>] categorize (in_progress)
131
+
132
+ <current_task name="categorize">
133
+ Organize the collected items into categories.
134
+ </current_task>
135
+
136
+ <rules>
137
+ Required order: collect -> categorize
138
+ Use update_task_status to advance. Do not skip tasks.
139
+ </rules>
140
+ </task_pipeline>
141
+ ```
142
+
143
+ Only the active task's tools (plus globals and `update_task_status`) are visible to the model.
144
+
145
+ ### Middleware hooks
146
+
147
+ | Hook | Behavior |
148
+ |---|---|
149
+ | `before_agent` | Initializes `task_statuses` in state on first invocation. |
150
+ | `wrap_model_call` | Appends task status board + active task instruction to system prompt. Filters tools to only the active task's tools + globals + `update_task_status`. Delegates to task-scoped middleware if present. |
151
+ | `wrap_tool_call` | Intercepts `update_task_status` — runs `validate_completion` on the task's scoped middleware before allowing completion. Rejects out-of-scope tool calls. Delegates other tool calls to the active task's scoped middleware. |
152
+ | `tools` | Auto-registers all task tools + globals + `update_task_status` with the agent. |
153
+
154
+ ### Task lifecycle
155
+
156
+ ```
157
+ PENDING ──> IN_PROGRESS ──> COMPLETE
158
+ ```
159
+
160
+ - The agent drives transitions by calling `update_task_status(task, status)`.
161
+ - Transitions are enforced: `pending -> in_progress -> complete` only.
162
+ - When `enforce_order=True`, a task cannot start until all preceding tasks are complete.
163
+ - On `complete`, the task's `middleware.validate_completion(state)` runs first — rejection returns an error to the agent without completing the transition.
164
+
165
+ ## Task-scoped middleware
166
+
167
+ Each task can have a `TaskMiddleware` that activates only when the task is `IN_PROGRESS`. This enables mid-task enforcement, not just completion gating.
168
+
169
+ ```python
170
+ from langchain.messages import ToolMessage
171
+ from task_steering import Task, TaskMiddleware, TaskSteeringMiddleware
172
+
173
+
174
+ class ThreatsMiddleware(TaskMiddleware):
175
+ """Block gap_analysis until enough threats exist."""
176
+
177
+ def __init__(self, min_threats: int = 25):
178
+ super().__init__()
179
+ self.min_threats = min_threats
180
+
181
+ def validate_completion(self, state) -> str | None:
182
+ threats = state.get("threats", [])
183
+ if len(threats) < self.min_threats:
184
+ return f"Only {len(threats)} threats — need at least {self.min_threats}."
185
+ return None
186
+
187
+ def wrap_tool_call(self, request, handler):
188
+ if request.tool_call["name"] == "gap_analysis":
189
+ threats = request.state.get("threats", [])
190
+ if len(threats) < self.min_threats:
191
+ return ToolMessage(
192
+ content=f"Cannot run gap_analysis: {len(threats)}/{self.min_threats} threats.",
193
+ tool_call_id=request.tool_call["id"],
194
+ )
195
+ return handler(request)
196
+
197
+
198
+ pipeline = TaskSteeringMiddleware(
199
+ tasks=[
200
+ Task(name="assets", instruction="...", tools=[create_assets]),
201
+ Task(
202
+ name="threats",
203
+ instruction="Identify STRIDE threats for each asset.",
204
+ tools=[create_threats, gap_analysis],
205
+ middleware=ThreatsMiddleware(min_threats=25),
206
+ ),
207
+ ],
208
+ )
209
+ ```
210
+
211
+ ### TaskMiddleware hooks
212
+
213
+ | Method | When it runs | Purpose |
214
+ |---|---|---|
215
+ | `validate_completion(state)` | Before `complete` transition | Return error string to reject, `None` to allow |
216
+ | `on_start(state)` | After successful `in_progress` transition | Side effects (logging, state init) |
217
+ | `on_complete(state)` | After successful `complete` transition | Side effects (trail capture, cleanup) |
218
+ | `wrap_tool_call(request, handler)` | On every tool call during this task | Mid-task tool gating / modification |
219
+ | `wrap_model_call(request, handler)` | On every model call during this task | Extra prompt injection / request modification |
220
+ | `state_schema` | At middleware init | Merge custom state fields into the agent's state |
221
+
222
+ ### Persistent state for task middleware
223
+
224
+ Task middleware can declare a `state_schema` to persist custom fields across interrupts:
225
+
226
+ ```python
227
+ from langchain.agents import AgentState
228
+ from typing_extensions import NotRequired
229
+
230
+
231
+ class ThreatsState(AgentState):
232
+ gap_analysis_uses: NotRequired[int]
233
+
234
+
235
+ class ThreatsMiddleware(TaskMiddleware):
236
+ state_schema = ThreatsState
237
+ # ...
238
+ ```
239
+
240
+ `TaskSteeringMiddleware` automatically merges all task middleware schemas into its own `state_schema`, so the fields survive checkpointing and interrupts.
241
+
242
+ ## Configuration
243
+
244
+ | Parameter | Default | Description |
245
+ |---|---|---|
246
+ | `tasks` | *(required)* | Ordered list of `Task` definitions. |
247
+ | `global_tools` | `[]` | Tools available in every task. |
248
+ | `enforce_order` | `True` | Require tasks to be completed in definition order. |
249
+
250
+ ### Task fields
251
+
252
+ | Field | Required | Description |
253
+ |---|---|---|
254
+ | `name` | yes | Unique identifier (used in prompts and state). |
255
+ | `instruction` | yes | Injected into system prompt when this task is active. |
256
+ | `tools` | yes | Tools visible when this task is `IN_PROGRESS`. |
257
+ | `middleware` | no | Scoped `TaskMiddleware` — only active during this task. |
258
+
259
+ ## Composability
260
+
261
+ `TaskSteeringMiddleware` is a standard `AgentMiddleware`. It composes with other LangChain v1 middleware:
262
+
263
+ ```python
264
+ from langchain.agents import create_agent
265
+ from langchain.agents.middleware import SummarizationMiddleware
266
+
267
+ agent = create_agent(
268
+ model="anthropic:claude-sonnet-4-6",
269
+ middleware=[
270
+ SummarizationMiddleware(
271
+ model="anthropic:claude-haiku-4-5-20251001",
272
+ trigger={"tokens": 8000},
273
+ ),
274
+ pipeline,
275
+ ],
276
+ )
277
+ ```
278
+
279
+ ## Development
280
+
281
+ ```bash
282
+ # Install with dev dependencies
283
+ pip install -e ".[dev]"
284
+
285
+ # Run tests
286
+ pytest
287
+
288
+ # Run tests with coverage
289
+ pytest --cov=task_steering
290
+ ```
291
+
292
+ ## Project structure
293
+
294
+ ```
295
+ task-steering/
296
+ src/task_steering/
297
+ __init__.py # Public exports
298
+ types.py # Task, TaskMiddleware, TaskStatus, TaskSteeringState
299
+ middleware.py # TaskSteeringMiddleware implementation
300
+ tests/
301
+ conftest.py # Fixtures and mock objects
302
+ test_middleware.py # Test suite
303
+ examples/
304
+ simple_agent.py # End-to-end example with Bedrock
305
+ pyproject.toml
306
+ ```
307
+
308
+ ## License
309
+
310
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,283 @@
1
+ # task-steering
2
+
3
+ Implicit state-machine middleware for [LangChain v1](https://python.langchain.com/) agents. Define ordered task pipelines with per-task tool scoping, dynamic prompt injection, and composable validation — all as a drop-in `AgentMiddleware`.
4
+
5
+ ```
6
+ PENDING ──> IN_PROGRESS ──> COMPLETE
7
+ ```
8
+
9
+ The model drives its own transitions by calling `update_task_status`. The middleware enforces ordering, scopes tools, injects the active task's instruction into the system prompt, and gates completion via pluggable validators.
10
+
11
+ ## When to use this
12
+
13
+ | Scenario | task-steering | LangGraph explicit workflows |
14
+ |---|:---:|:---:|
15
+ | Linear task pipeline (A then B then C) | **Best fit** | Verbose — one node + edges per task |
16
+ | Per-task tool scoping | **Built-in** | Manual — separate tool lists per node |
17
+ | Dynamic tasks from config / DB | **Easy** — tasks are data | Hard — graph is compiled at build time |
18
+ | Branching / parallel execution | Not supported | **Built-in** — edges + `Send()` |
19
+ | Per-task human-in-the-loop interrupts | Not supported | **Built-in** — `interrupt()` per node |
20
+ | Complex orchestration with retries / cycles | Not supported | **Built-in** — conditional edges |
21
+ | Composition with other middleware | **Native** — it's an `AgentMiddleware` | N/A — different abstraction |
22
+ | Debuggability in LangGraph Studio | Opaque — single agent node | **Clear** — each node visible in traces |
23
+
24
+ **Rule of thumb:** If your tasks are sequential and tool-scoped, use task-steering. If you need branching, parallelism, or per-task interrupts, use explicit LangGraph workflows.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install task-steering
30
+ ```
31
+
32
+ For development:
33
+
34
+ ```bash
35
+ git clone https://github.com/edvinhallvaxhiu/task-steering
36
+ cd task-steering
37
+ pip install -e ".[dev]"
38
+ ```
39
+
40
+ ### Requirements
41
+
42
+ - Python >= 3.10
43
+ - `langchain >= 1.0.0`
44
+ - `langgraph >= 0.4.0`
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ from langchain.agents import create_agent
50
+ from langchain.tools import tool
51
+ from task_steering import TaskSteeringMiddleware, Task
52
+
53
+
54
+ @tool
55
+ def add_items(items: list[str]) -> str:
56
+ """Add items to the inventory."""
57
+ return f"Added {len(items)} items."
58
+
59
+
60
+ @tool
61
+ def categorize(categories: dict[str, list[str]]) -> str:
62
+ """Assign items to categories."""
63
+ return f"Categorized into {len(categories)} groups."
64
+
65
+
66
+ pipeline = TaskSteeringMiddleware(
67
+ tasks=[
68
+ Task(
69
+ name="collect",
70
+ instruction="Collect all relevant items from the user's input.",
71
+ tools=[add_items],
72
+ ),
73
+ Task(
74
+ name="categorize",
75
+ instruction="Organize the collected items into categories.",
76
+ tools=[categorize],
77
+ ),
78
+ ],
79
+ )
80
+
81
+ agent = create_agent(
82
+ model="anthropic:claude-sonnet-4-6",
83
+ middleware=[pipeline],
84
+ system_prompt="You are an inventory assistant.",
85
+ )
86
+
87
+ result = agent.invoke(
88
+ {"messages": [{"role": "user", "content": "I have apples, bolts, and milk."}]}
89
+ )
90
+ ```
91
+
92
+ The agent automatically receives an `update_task_status` tool and sees a task pipeline block in its system prompt. It must complete `collect` before starting `categorize`.
93
+
94
+ ## How it works
95
+
96
+ ### What the model sees
97
+
98
+ Every model call, the middleware appends a status block to the system prompt:
99
+
100
+ ```xml
101
+ <task_pipeline>
102
+ [x] collect (complete)
103
+ [>] categorize (in_progress)
104
+
105
+ <current_task name="categorize">
106
+ Organize the collected items into categories.
107
+ </current_task>
108
+
109
+ <rules>
110
+ Required order: collect -> categorize
111
+ Use update_task_status to advance. Do not skip tasks.
112
+ </rules>
113
+ </task_pipeline>
114
+ ```
115
+
116
+ Only the active task's tools (plus globals and `update_task_status`) are visible to the model.
117
+
118
+ ### Middleware hooks
119
+
120
+ | Hook | Behavior |
121
+ |---|---|
122
+ | `before_agent` | Initializes `task_statuses` in state on first invocation. |
123
+ | `wrap_model_call` | Appends task status board + active task instruction to system prompt. Filters tools to only the active task's tools + globals + `update_task_status`. Delegates to task-scoped middleware if present. |
124
+ | `wrap_tool_call` | Intercepts `update_task_status` — runs `validate_completion` on the task's scoped middleware before allowing completion. Rejects out-of-scope tool calls. Delegates other tool calls to the active task's scoped middleware. |
125
+ | `tools` | Auto-registers all task tools + globals + `update_task_status` with the agent. |
126
+
127
+ ### Task lifecycle
128
+
129
+ ```
130
+ PENDING ──> IN_PROGRESS ──> COMPLETE
131
+ ```
132
+
133
+ - The agent drives transitions by calling `update_task_status(task, status)`.
134
+ - Transitions are enforced: `pending -> in_progress -> complete` only.
135
+ - When `enforce_order=True`, a task cannot start until all preceding tasks are complete.
136
+ - On `complete`, the task's `middleware.validate_completion(state)` runs first — rejection returns an error to the agent without completing the transition.
137
+
138
+ ## Task-scoped middleware
139
+
140
+ Each task can have a `TaskMiddleware` that activates only when the task is `IN_PROGRESS`. This enables mid-task enforcement, not just completion gating.
141
+
142
+ ```python
143
+ from langchain.messages import ToolMessage
144
+ from task_steering import Task, TaskMiddleware, TaskSteeringMiddleware
145
+
146
+
147
+ class ThreatsMiddleware(TaskMiddleware):
148
+ """Block gap_analysis until enough threats exist."""
149
+
150
+ def __init__(self, min_threats: int = 25):
151
+ super().__init__()
152
+ self.min_threats = min_threats
153
+
154
+ def validate_completion(self, state) -> str | None:
155
+ threats = state.get("threats", [])
156
+ if len(threats) < self.min_threats:
157
+ return f"Only {len(threats)} threats — need at least {self.min_threats}."
158
+ return None
159
+
160
+ def wrap_tool_call(self, request, handler):
161
+ if request.tool_call["name"] == "gap_analysis":
162
+ threats = request.state.get("threats", [])
163
+ if len(threats) < self.min_threats:
164
+ return ToolMessage(
165
+ content=f"Cannot run gap_analysis: {len(threats)}/{self.min_threats} threats.",
166
+ tool_call_id=request.tool_call["id"],
167
+ )
168
+ return handler(request)
169
+
170
+
171
+ pipeline = TaskSteeringMiddleware(
172
+ tasks=[
173
+ Task(name="assets", instruction="...", tools=[create_assets]),
174
+ Task(
175
+ name="threats",
176
+ instruction="Identify STRIDE threats for each asset.",
177
+ tools=[create_threats, gap_analysis],
178
+ middleware=ThreatsMiddleware(min_threats=25),
179
+ ),
180
+ ],
181
+ )
182
+ ```
183
+
184
+ ### TaskMiddleware hooks
185
+
186
+ | Method | When it runs | Purpose |
187
+ |---|---|---|
188
+ | `validate_completion(state)` | Before `complete` transition | Return error string to reject, `None` to allow |
189
+ | `on_start(state)` | After successful `in_progress` transition | Side effects (logging, state init) |
190
+ | `on_complete(state)` | After successful `complete` transition | Side effects (trail capture, cleanup) |
191
+ | `wrap_tool_call(request, handler)` | On every tool call during this task | Mid-task tool gating / modification |
192
+ | `wrap_model_call(request, handler)` | On every model call during this task | Extra prompt injection / request modification |
193
+ | `state_schema` | At middleware init | Merge custom state fields into the agent's state |
194
+
195
+ ### Persistent state for task middleware
196
+
197
+ Task middleware can declare a `state_schema` to persist custom fields across interrupts:
198
+
199
+ ```python
200
+ from langchain.agents import AgentState
201
+ from typing_extensions import NotRequired
202
+
203
+
204
+ class ThreatsState(AgentState):
205
+ gap_analysis_uses: NotRequired[int]
206
+
207
+
208
+ class ThreatsMiddleware(TaskMiddleware):
209
+ state_schema = ThreatsState
210
+ # ...
211
+ ```
212
+
213
+ `TaskSteeringMiddleware` automatically merges all task middleware schemas into its own `state_schema`, so the fields survive checkpointing and interrupts.
214
+
215
+ ## Configuration
216
+
217
+ | Parameter | Default | Description |
218
+ |---|---|---|
219
+ | `tasks` | *(required)* | Ordered list of `Task` definitions. |
220
+ | `global_tools` | `[]` | Tools available in every task. |
221
+ | `enforce_order` | `True` | Require tasks to be completed in definition order. |
222
+
223
+ ### Task fields
224
+
225
+ | Field | Required | Description |
226
+ |---|---|---|
227
+ | `name` | yes | Unique identifier (used in prompts and state). |
228
+ | `instruction` | yes | Injected into system prompt when this task is active. |
229
+ | `tools` | yes | Tools visible when this task is `IN_PROGRESS`. |
230
+ | `middleware` | no | Scoped `TaskMiddleware` — only active during this task. |
231
+
232
+ ## Composability
233
+
234
+ `TaskSteeringMiddleware` is a standard `AgentMiddleware`. It composes with other LangChain v1 middleware:
235
+
236
+ ```python
237
+ from langchain.agents import create_agent
238
+ from langchain.agents.middleware import SummarizationMiddleware
239
+
240
+ agent = create_agent(
241
+ model="anthropic:claude-sonnet-4-6",
242
+ middleware=[
243
+ SummarizationMiddleware(
244
+ model="anthropic:claude-haiku-4-5-20251001",
245
+ trigger={"tokens": 8000},
246
+ ),
247
+ pipeline,
248
+ ],
249
+ )
250
+ ```
251
+
252
+ ## Development
253
+
254
+ ```bash
255
+ # Install with dev dependencies
256
+ pip install -e ".[dev]"
257
+
258
+ # Run tests
259
+ pytest
260
+
261
+ # Run tests with coverage
262
+ pytest --cov=task_steering
263
+ ```
264
+
265
+ ## Project structure
266
+
267
+ ```
268
+ task-steering/
269
+ src/task_steering/
270
+ __init__.py # Public exports
271
+ types.py # Task, TaskMiddleware, TaskStatus, TaskSteeringState
272
+ middleware.py # TaskSteeringMiddleware implementation
273
+ tests/
274
+ conftest.py # Fixtures and mock objects
275
+ test_middleware.py # Test suite
276
+ examples/
277
+ simple_agent.py # End-to-end example with Bedrock
278
+ pyproject.toml
279
+ ```
280
+
281
+ ## License
282
+
283
+ MIT — see [LICENSE](LICENSE).