zjcode 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Goal tools exposed to the agent for persisted TUI goals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
Annotated,
|
|
8
|
+
Any,
|
|
9
|
+
Literal,
|
|
10
|
+
NotRequired,
|
|
11
|
+
TypedDict,
|
|
12
|
+
TypeVar,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from langchain.agents.middleware.types import (
|
|
17
|
+
AgentMiddleware,
|
|
18
|
+
ContextT,
|
|
19
|
+
ModelRequest,
|
|
20
|
+
ModelResponse,
|
|
21
|
+
)
|
|
22
|
+
from langchain_core.messages import SystemMessage, ToolMessage
|
|
23
|
+
from langchain_core.tools import InjectedToolCallId, tool
|
|
24
|
+
from langgraph.prebuilt import InjectedState
|
|
25
|
+
from langgraph.types import Command
|
|
26
|
+
from typing_extensions import override
|
|
27
|
+
|
|
28
|
+
# Runtime (not TYPE_CHECKING) imports. `GoalRubricChannels` supplies the shared
|
|
29
|
+
# `PrivateStateAttr`-marked goal/rubric channels that `GoalToolState` extends, so
|
|
30
|
+
# the markers are declared once (see that class). `coerce_goal_status` is used at
|
|
31
|
+
# runtime by `_goal_snapshot`; `GoalStatus` types its result and snapshot fields.
|
|
32
|
+
from deepagents_code.resume_state import (
|
|
33
|
+
GoalRubricChannels,
|
|
34
|
+
GoalStatus,
|
|
35
|
+
coerce_goal_status,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from collections.abc import Awaitable, Callable
|
|
40
|
+
|
|
41
|
+
RubricSource = Literal["goal", "sticky", "invocation"]
|
|
42
|
+
"""Where the active rubric criteria came from, as reported to the model."""
|
|
43
|
+
|
|
44
|
+
GOAL_TOOLS_SYSTEM_PROMPT = """## Goal and Rubric Tools
|
|
45
|
+
|
|
46
|
+
Use `get_rubric` to inspect active acceptance criteria before deciding whether work is
|
|
47
|
+
complete.
|
|
48
|
+
When a goal is active, use `get_goal` to inspect the objective and current status.
|
|
49
|
+
A paused goal is persisted for later but must not drive work until the user resumes it.
|
|
50
|
+
Use `update_goal` only when you have evidence that the goal is complete or blocked."""
|
|
51
|
+
"""Model-visible guidance injected before each request by `GoalToolsMiddleware`."""
|
|
52
|
+
|
|
53
|
+
ResponseT = TypeVar("ResponseT")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _runtime_blocked_goal_retry_context(ctx: object) -> str | None:
|
|
57
|
+
"""Return blocked-goal retry context from LangGraph runtime context."""
|
|
58
|
+
if isinstance(ctx, dict):
|
|
59
|
+
value = ctx.get("blocked_goal_retry_context")
|
|
60
|
+
else:
|
|
61
|
+
value = getattr(ctx, "blocked_goal_retry_context", None)
|
|
62
|
+
return value if isinstance(value, str) and value else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RubricSnapshot(TypedDict):
|
|
66
|
+
"""Read-only rubric view returned by the `get_rubric` tool to the model."""
|
|
67
|
+
|
|
68
|
+
active: bool
|
|
69
|
+
"""Whether acceptance criteria are currently available."""
|
|
70
|
+
|
|
71
|
+
criteria: str | None
|
|
72
|
+
"""Current acceptance criteria, or `None` when no rubric is set."""
|
|
73
|
+
|
|
74
|
+
source: RubricSource | None
|
|
75
|
+
"""Where the criteria came from: `goal`, `sticky`, `invocation`, or `None`."""
|
|
76
|
+
|
|
77
|
+
grading_status: str | None
|
|
78
|
+
"""Latest `RubricMiddleware` grading status for the in-progress or
|
|
79
|
+
just-completed graded turn, or `None`.
|
|
80
|
+
|
|
81
|
+
The middleware clears this at the start of the next graded turn, so
|
|
82
|
+
a `None` does not imply grading never ran.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GoalSnapshot(TypedDict):
|
|
87
|
+
"""Read-only goal view returned by the `get_goal` tool to the model.
|
|
88
|
+
|
|
89
|
+
A fixed-shape projection of goal state. Both construction branches in
|
|
90
|
+
`_goal_snapshot` must populate every key, so the type checker catches a
|
|
91
|
+
drift between them.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
active: bool
|
|
95
|
+
"""Whether the goal is actionable (should drive work).
|
|
96
|
+
|
|
97
|
+
Derived from `status`: `active` and `blocked` goals are actionable, while
|
|
98
|
+
`paused` and `complete` goals are not. Note a `paused` goal is unfinished
|
|
99
|
+
yet reports `active=False`. `False` when no goal is set (the
|
|
100
|
+
`objective is None` branch), where `status` is also `None`.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
objective: str | None
|
|
104
|
+
"""Active goal objective, or `None` when no goal is set."""
|
|
105
|
+
|
|
106
|
+
status: GoalStatus | None
|
|
107
|
+
"""Lifecycle status, or `None` when no goal is set.
|
|
108
|
+
|
|
109
|
+
A set-but-unlabeled or unrecognized persisted value is normalized to
|
|
110
|
+
`"active"` by `coerce_goal_status`, so this is always a known `GoalStatus`
|
|
111
|
+
when a goal is set.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
criteria: str | None
|
|
115
|
+
"""Persisted goal criteria, or shared rubric criteria when no goal rubric exists."""
|
|
116
|
+
|
|
117
|
+
note: str | None
|
|
118
|
+
"""Latest evidence or blocker note recorded by `update_goal`."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class GoalToolState(GoalRubricChannels):
|
|
122
|
+
"""State fields used by goal tools.
|
|
123
|
+
|
|
124
|
+
Inherits the shared `_goal_*`/`_sticky_rubric` channels (with their
|
|
125
|
+
`PrivateStateAttr` markers) from `GoalRubricChannels`, so the goal tools and
|
|
126
|
+
`ResumeState` cannot drift apart. Adds only the public `rubric` graph input,
|
|
127
|
+
which is intentionally non-private — it is the `RubricMiddleware` input.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
rubric: NotRequired[str | None]
|
|
131
|
+
"""Public `RubricMiddleware` graph input (intentionally non-private).
|
|
132
|
+
|
|
133
|
+
Distinct from the TUI-owned `_sticky_rubric`: this is the per-invocation
|
|
134
|
+
rubric passed in via the graph schema, not checkpointed TUI state.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _clean_state_text(state: dict[str, Any], key: str) -> str | None:
|
|
139
|
+
"""Return a non-empty string from state, or `None`."""
|
|
140
|
+
value = state.get(key)
|
|
141
|
+
if not isinstance(value, str):
|
|
142
|
+
return None
|
|
143
|
+
value = value.strip()
|
|
144
|
+
return value or None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _rubric_snapshot(state: dict[str, Any]) -> RubricSnapshot:
|
|
148
|
+
"""Build the `get_rubric` response from graph state.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
state: Current graph state injected by LangGraph.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Rubric snapshot visible to the model.
|
|
155
|
+
"""
|
|
156
|
+
criteria = _clean_state_text(state, "rubric")
|
|
157
|
+
goal_rubric = _clean_state_text(state, "_goal_rubric")
|
|
158
|
+
sticky_rubric = _clean_state_text(state, "_sticky_rubric")
|
|
159
|
+
objective = _clean_state_text(state, "_goal_objective")
|
|
160
|
+
status = coerce_goal_status(state.get("_goal_status")) or "active"
|
|
161
|
+
goal_is_actionable = objective is not None and status in {"active", "blocked"}
|
|
162
|
+
sticky_is_goal_rubric = objective is not None and sticky_rubric == goal_rubric
|
|
163
|
+
|
|
164
|
+
source: RubricSource | None = None
|
|
165
|
+
if criteria is not None:
|
|
166
|
+
if goal_is_actionable and goal_rubric == criteria:
|
|
167
|
+
source = "goal"
|
|
168
|
+
elif sticky_rubric == criteria and not sticky_is_goal_rubric:
|
|
169
|
+
source = "sticky"
|
|
170
|
+
else:
|
|
171
|
+
source = "invocation"
|
|
172
|
+
# Fallback branches below run only when there is no public `rubric` input,
|
|
173
|
+
# so `invocation` is unreachable here by construction — the criteria can
|
|
174
|
+
# only be attributed to an actionable `goal` or a standalone `sticky` rubric.
|
|
175
|
+
elif goal_is_actionable and goal_rubric is not None:
|
|
176
|
+
criteria = goal_rubric
|
|
177
|
+
source = "goal"
|
|
178
|
+
elif sticky_rubric is not None and not sticky_is_goal_rubric:
|
|
179
|
+
criteria = sticky_rubric
|
|
180
|
+
source = "sticky"
|
|
181
|
+
|
|
182
|
+
# `_rubric_status` is owned by the SDK's `RubricMiddleware`, co-composed into
|
|
183
|
+
# this agent's graph; see the `grading_status` field docstring above.
|
|
184
|
+
grading_status = _clean_state_text(state, "_rubric_status")
|
|
185
|
+
return {
|
|
186
|
+
"active": criteria is not None,
|
|
187
|
+
"criteria": criteria,
|
|
188
|
+
"source": source,
|
|
189
|
+
"grading_status": grading_status,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _goal_snapshot(state: dict[str, Any]) -> GoalSnapshot:
|
|
194
|
+
"""Build the `get_goal` response from graph state.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
state: Current graph state injected by LangGraph.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Goal snapshot visible to the model.
|
|
201
|
+
"""
|
|
202
|
+
objective = _clean_state_text(state, "_goal_objective")
|
|
203
|
+
rubric = _rubric_snapshot(state)
|
|
204
|
+
if objective is None:
|
|
205
|
+
return {
|
|
206
|
+
"active": False,
|
|
207
|
+
"objective": None,
|
|
208
|
+
"status": None,
|
|
209
|
+
"criteria": rubric["criteria"],
|
|
210
|
+
"note": None,
|
|
211
|
+
}
|
|
212
|
+
# A set-but-unlabeled or unrecognized status defaults to "active"; an
|
|
213
|
+
# unknown persisted value never leaks to the model as a bogus status.
|
|
214
|
+
status: GoalStatus = coerce_goal_status(state.get("_goal_status")) or "active"
|
|
215
|
+
criteria = _clean_state_text(state, "_goal_rubric") or rubric["criteria"]
|
|
216
|
+
note = _clean_state_text(state, "_goal_status_note")
|
|
217
|
+
return {
|
|
218
|
+
# Blocked goals remain actionable, while paused and complete goals do not
|
|
219
|
+
# drive work until the user changes their state.
|
|
220
|
+
"active": status in {"active", "blocked"},
|
|
221
|
+
"objective": objective,
|
|
222
|
+
"status": status,
|
|
223
|
+
"criteria": criteria,
|
|
224
|
+
"note": note,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _update_goal_command(
|
|
229
|
+
*,
|
|
230
|
+
status: Literal["complete", "blocked"],
|
|
231
|
+
note: str,
|
|
232
|
+
tool_call_id: str,
|
|
233
|
+
state: dict[str, Any],
|
|
234
|
+
) -> Command[Any]:
|
|
235
|
+
"""Build the constrained `update_goal` command.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
status: Goal status the model is reporting (`complete` or `blocked`).
|
|
239
|
+
note: Evidence the goal is complete, or the specific blocker. Required;
|
|
240
|
+
the status is not committed without it.
|
|
241
|
+
tool_call_id: Tool call ID for the returned `ToolMessage`.
|
|
242
|
+
state: Current graph state injected by LangGraph.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Command updating goal metadata and returning a tool response.
|
|
246
|
+
A `complete` request stages `_pending_goal_completion_note` for
|
|
247
|
+
the TUI to resolve once the rubric verdict lands, rather than
|
|
248
|
+
committing the status directly; `blocked` commits immediately.
|
|
249
|
+
|
|
250
|
+
When no goal is set or `note` is empty, nothing is committed
|
|
251
|
+
and the `ToolMessage` explains what the model must do instead.
|
|
252
|
+
"""
|
|
253
|
+
# Enforced preconditions here are only: an active goal exists and `note` is
|
|
254
|
+
# non-empty. Completion is staged because `RubricMiddleware` records its
|
|
255
|
+
# final verdict after the model stops making tool calls; the TUI resolves
|
|
256
|
+
# the staged request during post-turn checkpoint sync.
|
|
257
|
+
objective = state.get("_goal_objective")
|
|
258
|
+
if not isinstance(objective, str) or not objective:
|
|
259
|
+
return Command(
|
|
260
|
+
update={
|
|
261
|
+
"messages": [
|
|
262
|
+
ToolMessage(
|
|
263
|
+
content="No active goal is set.",
|
|
264
|
+
tool_call_id=tool_call_id,
|
|
265
|
+
)
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
goal_status = coerce_goal_status(state.get("_goal_status")) or "active"
|
|
270
|
+
if goal_status in {"paused", "complete"}:
|
|
271
|
+
if goal_status == "paused":
|
|
272
|
+
message = (
|
|
273
|
+
"The goal is paused. The user must run `/goal resume` before its "
|
|
274
|
+
"status can be updated."
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
message = "The goal is already complete and cannot be updated."
|
|
278
|
+
return Command(
|
|
279
|
+
update={
|
|
280
|
+
"messages": [ToolMessage(content=message, tool_call_id=tool_call_id)]
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
clean_note = note.strip()
|
|
284
|
+
if not clean_note:
|
|
285
|
+
# Evidence is required: refuse to commit a status with no justification
|
|
286
|
+
# rather than silently storing an empty note.
|
|
287
|
+
return Command(
|
|
288
|
+
update={
|
|
289
|
+
"messages": [
|
|
290
|
+
ToolMessage(
|
|
291
|
+
content=(
|
|
292
|
+
f"Provide a note with evidence before marking the "
|
|
293
|
+
f"goal {status}."
|
|
294
|
+
),
|
|
295
|
+
tool_call_id=tool_call_id,
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
if status == "complete":
|
|
301
|
+
return Command(
|
|
302
|
+
update={
|
|
303
|
+
"_pending_goal_completion_note": clean_note,
|
|
304
|
+
"messages": [
|
|
305
|
+
ToolMessage(
|
|
306
|
+
content=(
|
|
307
|
+
"Goal completion requested. It will be recorded if "
|
|
308
|
+
"the accepted rubric is satisfied."
|
|
309
|
+
),
|
|
310
|
+
tool_call_id=tool_call_id,
|
|
311
|
+
)
|
|
312
|
+
],
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
return Command(
|
|
316
|
+
update={
|
|
317
|
+
"_goal_status": status,
|
|
318
|
+
"_goal_status_note": clean_note,
|
|
319
|
+
"_pending_goal_completion_note": None,
|
|
320
|
+
"messages": [
|
|
321
|
+
ToolMessage(
|
|
322
|
+
content=f"Goal marked {status}. {clean_note}",
|
|
323
|
+
tool_call_id=tool_call_id,
|
|
324
|
+
)
|
|
325
|
+
],
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class GoalToolsMiddleware(AgentMiddleware[GoalToolState, ContextT]):
|
|
331
|
+
"""Expose constrained goal tools to the main agent."""
|
|
332
|
+
|
|
333
|
+
state_schema = GoalToolState
|
|
334
|
+
|
|
335
|
+
def __init__(self) -> None:
|
|
336
|
+
"""Initialize goal tools."""
|
|
337
|
+
super().__init__()
|
|
338
|
+
|
|
339
|
+
@tool
|
|
340
|
+
def get_rubric(
|
|
341
|
+
state: Annotated[dict[str, Any], InjectedState],
|
|
342
|
+
) -> RubricSnapshot:
|
|
343
|
+
"""Read the current acceptance criteria used to evaluate completion.
|
|
344
|
+
|
|
345
|
+
Call this to inspect the active rubric, whether it came from a goal,
|
|
346
|
+
a sticky rubric, or the current invocation, and the latest grading
|
|
347
|
+
status if a graded turn has already run.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Rubric snapshot with `active`, `criteria`, `source`, and
|
|
351
|
+
`grading_status` keys.
|
|
352
|
+
"""
|
|
353
|
+
return _rubric_snapshot(state)
|
|
354
|
+
|
|
355
|
+
@tool
|
|
356
|
+
def get_goal(
|
|
357
|
+
state: Annotated[dict[str, Any], InjectedState],
|
|
358
|
+
) -> GoalSnapshot:
|
|
359
|
+
"""Read the current persistent goal and acceptance criteria.
|
|
360
|
+
|
|
361
|
+
Call this before deciding whether work is done to see the objective,
|
|
362
|
+
the current acceptance criteria (which may come from the goal or a
|
|
363
|
+
standalone rubric), the current status, and any prior note.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Goal snapshot with `active`, `objective`, `status`, `criteria`,
|
|
367
|
+
and `note` keys.
|
|
368
|
+
"""
|
|
369
|
+
return _goal_snapshot(state)
|
|
370
|
+
|
|
371
|
+
@tool
|
|
372
|
+
def update_goal(
|
|
373
|
+
status: Literal["complete", "blocked"],
|
|
374
|
+
note: str,
|
|
375
|
+
tool_call_id: Annotated[str, InjectedToolCallId],
|
|
376
|
+
state: Annotated[dict[str, Any], InjectedState],
|
|
377
|
+
) -> Command[Any]:
|
|
378
|
+
"""Mark the current goal complete or blocked with evidence.
|
|
379
|
+
|
|
380
|
+
Use `complete` only when the accepted criteria are satisfied; use
|
|
381
|
+
`blocked` when you cannot proceed without user input. Completion is
|
|
382
|
+
rejected unless the latest rubric result is satisfied. Do not create,
|
|
383
|
+
pause, resume, clear, or replace goals — those are user-controlled.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
status: `complete` when the criteria are met, `blocked` when you
|
|
387
|
+
are stuck and need the user.
|
|
388
|
+
note: Evidence the criteria are satisfied, or the specific
|
|
389
|
+
blocker. Required — the status is not recorded without it.
|
|
390
|
+
tool_call_id: Injected tool call ID for the tool response.
|
|
391
|
+
state: Injected graph state holding the current goal.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Command that updates goal status and returns a tool message.
|
|
395
|
+
"""
|
|
396
|
+
return _update_goal_command(
|
|
397
|
+
status=status,
|
|
398
|
+
note=note,
|
|
399
|
+
tool_call_id=tool_call_id,
|
|
400
|
+
state=state,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
self.tools = [get_rubric, get_goal, update_goal]
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _request_with_goal_system_context(
|
|
407
|
+
request: ModelRequest[ContextT],
|
|
408
|
+
) -> ModelRequest[ContextT]:
|
|
409
|
+
"""Inject goal guidance and any one-turn retry context.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Model request with goal context appended to the system prompt.
|
|
413
|
+
"""
|
|
414
|
+
retry_context = _runtime_blocked_goal_retry_context(request.runtime.context)
|
|
415
|
+
prompt_parts = [GOAL_TOOLS_SYSTEM_PROMPT]
|
|
416
|
+
if retry_context is not None:
|
|
417
|
+
prompt_parts.append(retry_context)
|
|
418
|
+
prompt = "\n\n".join(prompt_parts)
|
|
419
|
+
|
|
420
|
+
if request.system_message is not None:
|
|
421
|
+
content = [
|
|
422
|
+
*request.system_message.content_blocks,
|
|
423
|
+
{"type": "text", "text": f"\n\n{prompt}"},
|
|
424
|
+
]
|
|
425
|
+
else:
|
|
426
|
+
content = [{"type": "text", "text": prompt}]
|
|
427
|
+
return request.override(
|
|
428
|
+
system_message=SystemMessage(
|
|
429
|
+
content=cast("list[str | dict[str, str]]", content)
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
@override
|
|
434
|
+
def wrap_model_call(
|
|
435
|
+
self,
|
|
436
|
+
request: ModelRequest[ContextT],
|
|
437
|
+
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
|
|
438
|
+
) -> ModelResponse[ResponseT]:
|
|
439
|
+
"""Inject goal-tool guidance into each model request.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Model response from the wrapped handler.
|
|
443
|
+
"""
|
|
444
|
+
return handler(self._request_with_goal_system_context(request))
|
|
445
|
+
|
|
446
|
+
@override
|
|
447
|
+
async def awrap_model_call(
|
|
448
|
+
self,
|
|
449
|
+
request: ModelRequest[ContextT],
|
|
450
|
+
handler: Callable[
|
|
451
|
+
[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]
|
|
452
|
+
],
|
|
453
|
+
) -> ModelResponse[ResponseT]:
|
|
454
|
+
"""Inject goal-tool guidance into each async model request.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Model response from the wrapped handler.
|
|
458
|
+
"""
|
|
459
|
+
return await handler(self._request_with_goal_system_context(request))
|