|
| 1 | +## Goal Tool Family (Persistent Session Goal) |
| 2 | + |
| 3 | +`GoalToolSet` exposes three tools — `create_goal`, `get_goal`, `update_goal` — aligned with Claude Code **Session Goal** capabilities. Unlike `TodoWriteTool` (multi-item checklist) and `TaskToolSet` (multi-task board), Goal maintains **at most one** persistent objective per session branch: while status is `active`, a response that **looks like a final answer** does **not** mean the work is done — the model must keep working or explicitly call `update_goal('complete' | 'blocked')`. |
| 4 | + |
| 5 | +The goal is serialized as a **single JSON blob** (`GoalRecord`) in `tool_context.state["goal[:<branch>]"]`, surviving across `Runner.run_async` calls. Beyond the three model tools, the full capability requires `setup_goal()` to mount **enforcement callbacks** (`before_model` / `after_model`) that intercept premature final responses and re-run within the **same invocation**. |
| 6 | + |
| 7 | +### Features |
| 8 | + |
| 9 | +- **Single-goal contract**: one `GoalRecord` per branch (`objective` + three states `active` / `complete` / `blocked`); `complete` / `blocked` are **irreversible** terminal states |
| 10 | +- **Cross-turn persistence**: persisted via function-response state deltas; **do not** use the `temp:` prefix |
| 11 | +- **Sub-agent isolation**: state key appends `:<branch>` |
| 12 | +- **Enforced completion**: while `active`, `after_model` detects premature finals (no tool call, visible text, non-partial), suppresses them, and re-runs in the same invocation; `before_model` injects a user-role nudge |
| 13 | +- **Fail-open budget**: after `max_retries` (default 3) interceptions, the final response is allowed so the loop cannot spin forever; counters live in invocation-scoped `agent_context.metadata` and are not persisted |
| 14 | +- **Two creation paths**: |
| 15 | + - **Model side**: `create_goal(objective=...)` — LLM creates after judging a multi-step task |
| 16 | + - **Host side**: `start_goal(session_service, ...)` — application writes the goal before the first turn; the model does not call `create_goal` |
| 17 | +- **Layered prompt guidance**: `DEFAULT_GUIDANCE` injected into system instruction via `before_model` when `inject_guidance=True`; hard rules enforced by store validation + callbacks |
| 18 | +- **Concurrency safety**: `_GoalToolBase` wraps load → mutate → save in `goal_store_lock` (per session + branch), compatible with `parallel_tool_calls=True` |
| 19 | + |
| 20 | +### Relationship to Todo / Task |
| 21 | + |
| 22 | +| Dimension | `TodoWriteTool` | `TaskToolSet` | Goal Tool Family | |
| 23 | +| --- | --- | --- | --- | |
| 24 | +| Granularity | Multi-item checklist | Multi-task board + deps | **Single** session objective | |
| 25 | +| Update style | Full-list replace | Incremental by `taskId` | `create_goal` / `update_goal` | |
| 26 | +| Can finish while incomplete? | Prompt guidance | Prompt guidance | **Callback enforcement** | |
| 27 | +| State key | `todos[:branch]` | `tasks[:branch]` | `goal[:branch]` | |
| 28 | +| Typical use | Step visibility, short lists | Long boards, dependencies | Whether the whole job is truly done | |
| 29 | + |
| 30 | +> Todo / Task handle **step decomposition**; Goal handles the **overall completion contract**. They can be combined, but avoid mounting too many planning tools at once. |
| 31 | +
|
| 32 | +### GoalOptions Constructor Parameters |
| 33 | + |
| 34 | +Configure via `setup_goal(agent, GoalOptions(...))`: |
| 35 | + |
| 36 | +| Parameter | Type | Default | Description | |
| 37 | +|------|------|--------|------| |
| 38 | +| `state_key_prefix` | `str` | `"goal"` | State key prefix; do not use `temp:` | |
| 39 | +| `inject_guidance` | `bool` | `True` | Inject `DEFAULT_GUIDANCE` into system instruction in `before_model` | |
| 40 | +| `guidance` | `str` | `DEFAULT_GUIDANCE` | Long guidance text (serial goal-tool calls, etc.) | |
| 41 | +| `max_retries` | `int` | `3` | Same-invocation budget for intercepting premature finals; fail-open when exhausted | |
| 42 | +| `nudge_template` | `str` | `DEFAULT_NUDGE` | User-role reminder after interception; supports `{attempt}` / `{max_retries}` / `{objective}` | |
| 43 | +| `on_retry` | `Callable[[RetryEvent], None]` | `None` | Observability callback on each interception or budget exhaustion | |
| 44 | + |
| 45 | +Mounting only `GoalToolSet()` without enforcement gives model-facing tools but **not** "no final while active". |
| 46 | + |
| 47 | +### LLM Parameters for the Three Tools |
| 48 | + |
| 49 | +**`create_goal`** |
| 50 | + |
| 51 | +| Parameter | Required | Description | |
| 52 | +|------|------|------| |
| 53 | +| `objective` | Yes | Completion criteria — what "done" concretely means | |
| 54 | + |
| 55 | +Success: `{message, goal}`; if an `active` goal already exists: `{error: "INVALID_STATE: ..."}`. |
| 56 | + |
| 57 | +**`get_goal`** |
| 58 | + |
| 59 | +No parameters. With a goal: `{message, goal}`; without: `{message: "No session goal is set."}`. |
| 60 | + |
| 61 | +**`update_goal`** |
| 62 | + |
| 63 | +| Parameter | Required | Description | |
| 64 | +|------|------|------| |
| 65 | +| `status` | Yes | `complete` (objective met) or `blocked` (same blocker repeats; cannot proceed without user input) | |
| 66 | + |
| 67 | +Success: `{message, goal}`; no active goal or already terminal: `{error: "INVALID_STATE: ..."}`. |
| 68 | + |
| 69 | +**`GoalRecord` fields** (persisted with camelCase JSON aliases): |
| 70 | + |
| 71 | +| Field | Description | |
| 72 | +|------|------| |
| 73 | +| `id` | Server-assigned uuid | |
| 74 | +| `objective` | Completion criteria text | |
| 75 | +| `status` | `active` / `complete` / `blocked` | |
| 76 | +| `createdAtUnix` / `updatedAtUnix` | Created / last-updated time (unix seconds) | |
| 77 | +| `terminalAtUnix` | Time entered a terminal state (optional) | |
| 78 | + |
| 79 | +### Enforcement Workflow |
| 80 | + |
| 81 | +```text |
| 82 | +Model outputs final text (no tool call, goal still active) |
| 83 | + ↓ |
| 84 | +after_model classifies as premature final |
| 85 | + ↓ |
| 86 | +Suppress final (not committed as answer), retry_count += 1 |
| 87 | +before_model injects nudge, same invocation continues agent loop |
| 88 | + ↓ |
| 89 | +retry_count >= max_retries → fail-open, on_retry(reason="exhausted") |
| 90 | +``` |
| 91 | + |
| 92 | +Interception condition (`_is_premature_final`): non-partial, no error, visible text in content, and **no** `function_call` / `function_response`. |
| 93 | + |
| 94 | +### Usage |
| 95 | + |
| 96 | +Recommended one-line mount of tools + callbacks: |
| 97 | + |
| 98 | +```python |
| 99 | +from trpc_agent_sdk.agents import LlmAgent |
| 100 | +from trpc_agent_sdk.models import OpenAIModel |
| 101 | +from trpc_agent_sdk.tools.goal_tools import GoalOptions, RetryEvent, setup_goal |
| 102 | + |
| 103 | +def on_retry(event: RetryEvent) -> None: |
| 104 | + if event.reason == "blocked": |
| 105 | + print(f"Premature final intercepted (attempt {event.attempt_number}/{event.max_retries})") |
| 106 | + |
| 107 | +agent = LlmAgent( |
| 108 | + name="goal_agent", |
| 109 | + model=OpenAIModel(model_name="...", api_key="...", base_url="..."), |
| 110 | + instruction="Use goal tools to track completion for multi-step engineering tasks.", |
| 111 | + tools=[...], # Business tools, e.g. BashTool / WriteTool |
| 112 | +) |
| 113 | +setup_goal(agent, GoalOptions(max_retries=3, on_retry=on_retry)) |
| 114 | +``` |
| 115 | + |
| 116 | +Host pre-injects a goal before the first turn (model does not call `create_goal`): |
| 117 | + |
| 118 | +```python |
| 119 | +from trpc_agent_sdk.tools.goal_tools import start_goal |
| 120 | + |
| 121 | +goal = await start_goal( |
| 122 | + session_service, |
| 123 | + app_name="my_app", |
| 124 | + user_id="user_1", |
| 125 | + session_id=session_id, |
| 126 | + objective="Create notes/ with summary.txt and example.py in the current directory", |
| 127 | + agent_name=agent.name, # Match LlmAgent.name for branch isolation |
| 128 | +) |
| 129 | +``` |
| 130 | + |
| 131 | +Read back the persisted goal (REST / audit / demo wrap-up): |
| 132 | + |
| 133 | +```python |
| 134 | +from trpc_agent_sdk.tools.goal_tools import get_goal_record, render_goal |
| 135 | + |
| 136 | +goal = get_goal_record(session, branch=agent.name) |
| 137 | +print(render_goal(goal)) |
| 138 | +# ✅ Goal [complete] |
| 139 | +# objective: ... |
| 140 | +# created: 1782893110 |
| 141 | +# terminal: 1782893116 |
| 142 | +``` |
| 143 | + |
| 144 | +### Goal Tool Family Best Practices |
| 145 | + |
| 146 | +- **Use `setup_goal`, not `GoalToolSet` alone**: only callbacks enforce "no final while active" |
| 147 | +- **Model vs host**: slash commands, `/goal`, config-driven tasks → `start_goal()`; let the model judge multi-step work → `create_goal` |
| 148 | +- **One goal tool per response**: `DEFAULT_GUIDANCE` requires serial semantics; do not call `create_goal` and `update_goal` in the same turn |
| 149 | +- **Use `blocked` sparingly**: only when the same blocker repeats across attempts and user input or external state change is required; do not mark blocked because work is hard, slow, or incomplete |
| 150 | +- **Observability**: use `on_retry` to log premature-final interceptions and budget exhaustion when tuning prompt or `max_retries` |
| 151 | +- **Division of labor with Todo / Task**: Todo / Task show steps and dependencies; Goal constrains whether the whole job is truly finished |
| 152 | + |
| 153 | +### Goal Tool Family Complete Example |
| 154 | + |
| 155 | +| Example | Description | |
| 156 | +| --- | --- | |
| 157 | +| [examples/goal_tools](../../../examples/goal_tools/) | Case 1: model `create_goal`; Case 2: host `start_goal` pre-injection; demonstrates enforcement interception and `update_goal(complete)` | |
0 commit comments