sofhiaazzhr Claude Opus 4.7 commited on
Commit
0cb4e6d
·
1 Parent(s): db217d6

[KM-627] Canonical ToolSpec + ToolOutput

Browse files

Move ToolSpec/ToolRegistry/ToolOutput to src/tools/contracts.py as the single
source of truth (owned by the tool team, KM-465). planner/contracts.py now
re-exports them so the planner/slow_path importers are unchanged; shapes are
identical to the prior stubs (zero field change).

Records the data-flow decision: Pattern A (analyze_* take data="${t<id>}"
placeholder, not self-fetch by source_id) and analyze_comparison.output_kind =
"stats", aligning with the planner registry.

Verified: imports OK, ruff + mypy strict clean, 72 analytics tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

src/agents/planner/contracts.py CHANGED
@@ -1,32 +1,35 @@
1
- """STUB contracts the planner consumes from other teams.
2
 
3
- These models are LOCAL STUBS so the planner is buildable and testable now.
4
- They must be reconciled with their owners before integration:
 
5
 
6
- - `BusinessContext` (+ KeyTerm / DataTableNote / DataColumnNote)
7
- Owner: the lead (interview / Business Understanding).
8
- Shape mirrors AGENT_ARCHITECTURE_CONTEXT_new.md §7.1.
9
-
10
- - `ToolSpec` / `ToolRegistry`
11
- Owner: the tool team (KM-608).
12
- Shape mirrors §9.2. The concrete v1 P0 registry instance lives in
13
- `registry.py`; this file only defines the contract types.
14
-
15
- - `ToolOutput`
16
- The tool -> agent output envelope (§8.1). Not produced by the planner
17
- (tools return it at TaskRunner time); defined here so the planner layer
18
- owns one definition of the contract it plans against.
19
-
20
- When the real modules land, delete the corresponding stub here and import from
21
- the shared location instead.
22
  """
23
 
24
  from __future__ import annotations
25
 
26
- from typing import Any, Literal
27
 
28
  from pydantic import BaseModel, Field
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # --------------------------------------------------------------------------- #
31
  # BusinessContext (lead's contract — §7.1)
32
  # --------------------------------------------------------------------------- #
@@ -60,45 +63,3 @@ class BusinessContext(BaseModel):
60
  recent_events: str = ""
61
  things_to_watch_for: str = ""
62
  open_questions: list[str] = Field(default_factory=list)
63
-
64
-
65
- # --------------------------------------------------------------------------- #
66
- # Tool registry (tool team's contract — §9.2)
67
- # --------------------------------------------------------------------------- #
68
-
69
-
70
- class ToolSpec(BaseModel):
71
- name: str
72
- category: str # analytics.query | .aggregation | .timeseries | ...
73
- input_schema: dict[str, Any] # validates ToolCall.args
74
- output_kind: str # the ToolOutput.kind it returns
75
- description: str # prompt-style: what it does, edge cases, what NOT to use it for
76
- phase: Literal["P0", "P1", "P2"] = "P0"
77
-
78
-
79
- class ToolRegistry(BaseModel):
80
- tools: list[ToolSpec] = Field(default_factory=list)
81
-
82
- def names(self) -> set[str]:
83
- return {t.name for t in self.tools}
84
-
85
- def get(self, name: str) -> ToolSpec | None:
86
- for t in self.tools:
87
- if t.name == name:
88
- return t
89
- return None
90
-
91
-
92
- # --------------------------------------------------------------------------- #
93
- # Tool output envelope (tool -> agent contract — §8.1)
94
- # --------------------------------------------------------------------------- #
95
-
96
-
97
- class ToolOutput(BaseModel):
98
- tool: str
99
- kind: Literal["scalar", "table", "stats", "series", "documents", "error"]
100
- value: Any | None = None
101
- columns: list[str] | None = None
102
- rows: list[list[Any]] | None = None
103
- meta: dict[str, Any] = Field(default_factory=dict)
104
- error: str | None = None
 
1
+ """Contracts the planner consumes from other teams.
2
 
3
+ `BusinessContext` (+ KeyTerm / DataTableNote / DataColumnNote) is still a LOCAL
4
+ STUB owned by the lead (interview / Business Understanding); shape mirrors
5
+ AGENT_ARCHITECTURE_CONTEXT_new.md §7.1 and must be reconciled before integration.
6
 
7
+ `ToolSpec` / `ToolRegistry` / `ToolOutput` are NO LONGER defined here — the tool
8
+ team owns them (KM-465). They now live in `src.tools.contracts` and are
9
+ re-exported below so existing importers (`registry.py`, the slow-path
10
+ `invoker.py` / `schemas.py`) keep working against one shared definition.
 
 
 
 
 
 
 
 
 
 
 
 
11
  """
12
 
13
  from __future__ import annotations
14
 
15
+ from typing import Literal
16
 
17
  from pydantic import BaseModel, Field
18
 
19
+ # Canonical tool contracts now owned by the tool team (KM-465). Re-exported here
20
+ # for backwards-compatible imports; the definitions live in src.tools.contracts.
21
+ from src.tools.contracts import ToolOutput, ToolRegistry, ToolSpec
22
+
23
+ __all__ = [
24
+ "KeyTerm",
25
+ "DataTableNote",
26
+ "DataColumnNote",
27
+ "BusinessContext",
28
+ "ToolSpec",
29
+ "ToolRegistry",
30
+ "ToolOutput",
31
+ ]
32
+
33
  # --------------------------------------------------------------------------- #
34
  # BusinessContext (lead's contract — §7.1)
35
  # --------------------------------------------------------------------------- #
 
63
  recent_events: str = ""
64
  things_to_watch_for: str = ""
65
  open_questions: list[str] = Field(default_factory=list)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/tools/contracts.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical tool contracts (KM-465 — owned by the tool team).
2
+
3
+ These are the single source of truth for the tool <-> agent interface:
4
+
5
+ - `ToolSpec` / `ToolRegistry` — the registry contract (§9.2). The concrete v1
6
+ analytics registry instance is built on top of these.
7
+ - `ToolOutput` — the tool -> agent output envelope (§8.1). Tools return this at
8
+ TaskRunner time; the agent layer plans and degrades against it.
9
+
10
+ Ownership note (2026-06-08): the tool team owns these definitions outright; the
11
+ agent team's earlier copies in `src/agents/planner/contracts.py` are now thin
12
+ re-exports of this module so there is exactly ONE definition across the codebase.
13
+ The shapes here are kept byte-for-byte identical to those original stubs so the
14
+ already-landed planner / TaskRunner / Assembler keep working unchanged.
15
+
16
+ Data-flow decision (KM-465, agreed with the agent team): the analytics tools use
17
+ **Pattern A** — `analyze_*` tools do NOT self-fetch by `source_id`; each takes a
18
+ `data` argument that is a `"${t<id>}"` placeholder resolved to a DataFrame at
19
+ execution time. `analyze_comparison.output_kind` is `"stats"` (a labelled-metric
20
+ dict), aligning with the planner registry.
21
+
22
+ See AGENT_ARCHITECTURE_CONTEXT_new.md §8.1 / §9.2.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Literal
28
+
29
+ from pydantic import BaseModel, Field
30
+
31
+ # --------------------------------------------------------------------------- #
32
+ # Tool registry (§9.2)
33
+ # --------------------------------------------------------------------------- #
34
+
35
+
36
+ class ToolSpec(BaseModel):
37
+ name: str
38
+ category: str # analytics.query | .aggregation | .timeseries | ...
39
+ input_schema: dict[str, Any] # validates ToolCall.args
40
+ output_kind: str # the ToolOutput.kind it returns
41
+ description: str # prompt-style: what it does, edge cases, what NOT to use it for
42
+ phase: Literal["P0", "P1", "P2"] = "P0"
43
+
44
+
45
+ class ToolRegistry(BaseModel):
46
+ tools: list[ToolSpec] = Field(default_factory=list)
47
+
48
+ def names(self) -> set[str]:
49
+ return {t.name for t in self.tools}
50
+
51
+ def get(self, name: str) -> ToolSpec | None:
52
+ for t in self.tools:
53
+ if t.name == name:
54
+ return t
55
+ return None
56
+
57
+
58
+ # --------------------------------------------------------------------------- #
59
+ # Tool output envelope (§8.1)
60
+ # --------------------------------------------------------------------------- #
61
+
62
+
63
+ class ToolOutput(BaseModel):
64
+ tool: str
65
+ kind: Literal["scalar", "table", "stats", "series", "documents", "error"]
66
+ value: Any | None = None
67
+ columns: list[str] | None = None
68
+ rows: list[list[Any]] | None = None
69
+ meta: dict[str, Any] = Field(default_factory=dict)
70
+ error: str | None = None