linear-events 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,38 @@
1
+ Metadata-Version: 2.3
2
+ Name: linear-events
3
+ Version: 0.1.0
4
+ Summary: Linear webhook dispatch: HMAC-SHA256 signature verification, replay protection, and .on() handler routing
5
+ Requires-Dist: httpx>=0.28 ; extra == 'client'
6
+ Requires-Dist: fastapi>=0.110 ; extra == 'fastapi'
7
+ Requires-Python: >=3.11
8
+ Provides-Extra: client
9
+ Provides-Extra: fastapi
10
+ Description-Content-Type: text/markdown
11
+
12
+ # linear-events
13
+
14
+ Linear webhook payload types, HMAC signature verification, replay protection,
15
+ and event dispatch.
16
+
17
+ ```sh
18
+ pip install linear-events
19
+ # Optional FastAPI integration and agent activity client:
20
+ pip install 'linear-events[fastapi,client]'
21
+ ```
22
+
23
+ ```python
24
+ import os
25
+ from linear_events import LinearDispatcher
26
+
27
+ hooks = LinearDispatcher(signing_secret=os.environ["LINEAR_WEBHOOK_SECRET"])
28
+
29
+ @hooks.on("Issue", action="create")
30
+ async def on_issue(payload):
31
+ print(payload["data"])
32
+ ```
33
+
34
+ The dispatcher verifies signatures and timestamps when a signing secret is
35
+ configured. Mount `as_fastapi_router()` for FastAPI handling, or call `dispatch`
36
+ with the raw request body, signature, and timestamp yourself. The base package
37
+ has no runtime dependencies. `linear_events.client.LinearAgentClient` is
38
+ available with the `client` extra.
@@ -0,0 +1,27 @@
1
+ # linear-events
2
+
3
+ Linear webhook payload types, HMAC signature verification, replay protection,
4
+ and event dispatch.
5
+
6
+ ```sh
7
+ pip install linear-events
8
+ # Optional FastAPI integration and agent activity client:
9
+ pip install 'linear-events[fastapi,client]'
10
+ ```
11
+
12
+ ```python
13
+ import os
14
+ from linear_events import LinearDispatcher
15
+
16
+ hooks = LinearDispatcher(signing_secret=os.environ["LINEAR_WEBHOOK_SECRET"])
17
+
18
+ @hooks.on("Issue", action="create")
19
+ async def on_issue(payload):
20
+ print(payload["data"])
21
+ ```
22
+
23
+ The dispatcher verifies signatures and timestamps when a signing secret is
24
+ configured. Mount `as_fastapi_router()` for FastAPI handling, or call `dispatch`
25
+ with the raw request body, signature, and timestamp yourself. The base package
26
+ has no runtime dependencies. `linear_events.client.LinearAgentClient` is
27
+ available with the `client` extra.
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "linear-events"
3
+ version = "0.1.0"
4
+ description = "Linear webhook dispatch: HMAC-SHA256 signature verification, replay protection, and .on() handler routing"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = []
8
+
9
+ [project.optional-dependencies]
10
+ fastapi = ["fastapi>=0.110"]
11
+ client = ["httpx>=0.28"]
12
+
13
+ [dependency-groups]
14
+ dev = [
15
+ "fastapi>=0.110",
16
+ "httpx>=0.28",
17
+ "pytest>=9.0",
18
+ "pytest-asyncio>=1.4",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.11,<0.12"]
23
+ build-backend = "uv_build"
24
+
25
+ [tool.pytest.ini_options]
26
+ asyncio_mode = "auto"
27
+ testpaths = ["tests"]
@@ -0,0 +1,39 @@
1
+ """linear_events: Linear webhook signature verification and .on() dispatch.
2
+
3
+ Core (no dependencies): LinearDispatcher (verify + route by (type, action))
4
+ and the payload TypedDicts, including the AgentSessionEvent model.
5
+ linear-events[fastapi] adds a mountable endpoint via .as_fastapi_router();
6
+ linear-events[client] adds LinearAgentClient for emitting agent activities.
7
+ """
8
+
9
+ from linear_events.dispatch import (
10
+ LinearDispatcher,
11
+ LinearError,
12
+ SignatureVerificationError,
13
+ )
14
+ from linear_events.payloads import (
15
+ Actor,
16
+ AgentActivityWebhookPayload,
17
+ AgentSessionEventWebhookPayload,
18
+ AgentSessionWebhookPayload,
19
+ CommentChildWebhookPayload,
20
+ GuidanceRuleWebhookPayload,
21
+ IssueChildWebhookPayload,
22
+ UserChildWebhookPayload,
23
+ WebhookPayload,
24
+ )
25
+
26
+ __all__ = [
27
+ "Actor",
28
+ "AgentActivityWebhookPayload",
29
+ "AgentSessionEventWebhookPayload",
30
+ "AgentSessionWebhookPayload",
31
+ "CommentChildWebhookPayload",
32
+ "GuidanceRuleWebhookPayload",
33
+ "IssueChildWebhookPayload",
34
+ "LinearDispatcher",
35
+ "LinearError",
36
+ "SignatureVerificationError",
37
+ "UserChildWebhookPayload",
38
+ "WebhookPayload",
39
+ ]
@@ -0,0 +1,152 @@
1
+ """A minimal client for emitting Agent Activities (linear-events[client]).
2
+
3
+ Agents answer an AgentSession by posting typed activities — thought, action,
4
+ elicitation, response, error — which Linear renders in place and uses to
5
+ track the session's lifecycle. The content constructors below build exactly
6
+ the shapes Linear validates server-side.
7
+
8
+ client = LinearAgentClient(token)
9
+ await client.emit(session_id, thought("Looking into it…"))
10
+ await client.emit(session_id, action("Searching", "ENG-123"))
11
+ await client.emit(session_id, response("Done."))
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ try:
19
+ import httpx
20
+ except ImportError as e: # pragma: no cover
21
+ raise ImportError(
22
+ "linear_events.client requires httpx — install linear-events[client]"
23
+ ) from e
24
+
25
+ __all__ = [
26
+ "Activity",
27
+ "LinearAgentClient",
28
+ "LinearApiError",
29
+ "action",
30
+ "elicitation",
31
+ "error",
32
+ "response",
33
+ "thought",
34
+ ]
35
+
36
+ Activity = dict[str, Any] # the `content` of an agentActivityCreate
37
+
38
+
39
+ def thought(body: str, *, ephemeral: bool = False) -> Activity:
40
+ """An internal note. Ephemeral thoughts are replaced by the next activity."""
41
+ return {"type": "thought", "body": body, "ephemeral": ephemeral}
42
+
43
+
44
+ def elicitation(body: str) -> Activity:
45
+ """Ask the user for clarification or confirmation (session awaits input)."""
46
+ return {"type": "elicitation", "body": body}
47
+
48
+
49
+ def action(name: str, parameter: str, result: str | None = None, *, ephemeral: bool = False) -> Activity:
50
+ """A tool invocation, without (running) or with (completed) a result."""
51
+ activity: Activity = {"type": "action", "action": name, "parameter": parameter, "ephemeral": ephemeral}
52
+ if result is not None:
53
+ activity["result"] = result
54
+ return activity
55
+
56
+
57
+ def response(body: str) -> Activity:
58
+ """The final answer; marks the session's work complete. Markdown OK."""
59
+ return {"type": "response", "body": body}
60
+
61
+
62
+ def error(body: str) -> Activity:
63
+ """Report a failure; puts the session in its error state. Markdown OK."""
64
+ return {"type": "error", "body": body}
65
+
66
+
67
+ class LinearApiError(Exception):
68
+ """Linear answered a GraphQL call with an `errors` array."""
69
+
70
+
71
+ AGENT_ACTIVITY_CREATE = """
72
+ mutation AgentActivityCreate($input: AgentActivityCreateInput!) {
73
+ agentActivityCreate(input: $input) {
74
+ success
75
+ agentActivity { id }
76
+ }
77
+ }
78
+ """
79
+
80
+
81
+ class LinearAgentClient:
82
+ """Emits activities with an app-user OAuth token (Bearer). Pass an
83
+ existing httpx.AsyncClient as `http` to share one; otherwise each call
84
+ uses a short-lived client."""
85
+
86
+ def __init__(
87
+ self,
88
+ token: str,
89
+ *,
90
+ api_base: str = "https://api.linear.app",
91
+ http: httpx.AsyncClient | None = None,
92
+ ) -> None:
93
+ self._token = token
94
+ self._api_base = api_base.rstrip("/")
95
+ self._http = http
96
+
97
+ async def emit(self, agent_session_id: str, content: Activity, *, activity_id: str | None = None) -> dict[str, Any]:
98
+ """POST agentActivityCreate; returns the created activity's data."""
99
+ content = dict(content)
100
+ ephemeral = content.pop("ephemeral", None)
101
+ activity_input = {"agentSessionId": agent_session_id, "content": content}
102
+ if ephemeral is not None:
103
+ activity_input["ephemeral"] = ephemeral
104
+ if activity_id is not None:
105
+ activity_input["id"] = activity_id
106
+ try:
107
+ data = await self._graphql(AGENT_ACTIVITY_CREATE, {"input": activity_input})
108
+ result = data["agentActivityCreate"]
109
+ if not result.get("success") or not result.get("agentActivity"):
110
+ raise LinearApiError("Linear did not accept the activity")
111
+ return result["agentActivity"]
112
+ except (httpx.HTTPError, LinearApiError):
113
+ # An earlier attempt may have succeeded before its response was
114
+ # lost. Confirm the stable id instead of creating a second reply.
115
+ if activity_id is not None:
116
+ data = await self._graphql(
117
+ "query MoActivity($id: String!) { agentActivity(id: $id) { id agentSession { id } } }",
118
+ {"id": activity_id},
119
+ )
120
+ existing = data.get("agentActivity")
121
+ if existing and existing["agentSession"]["id"] == agent_session_id:
122
+ return {"id": existing["id"]}
123
+ raise
124
+
125
+ async def link(self, agent_session_id: str, url: str, label: str) -> None:
126
+ """Add a dashboard link without replacing links contributed elsewhere."""
127
+ data = await self._graphql(
128
+ "mutation MoSessionLink($id: String!, $input: AgentSessionUpdateInput!) { agentSessionUpdate(id: $id, input: $input) { success } }",
129
+ {"id": agent_session_id, "input": {"addedExternalUrls": [{"url": url, "label": label}]}},
130
+ )
131
+ if not data["agentSessionUpdate"]["success"]:
132
+ raise LinearApiError("Linear did not accept the session link")
133
+
134
+ async def _graphql(self, query: str, variables: dict[str, Any]) -> Any:
135
+ async def call(http: httpx.AsyncClient) -> httpx.Response:
136
+ return await http.post(
137
+ f"{self._api_base}/graphql",
138
+ headers={"Authorization": f"Bearer {self._token}"},
139
+ json={"query": query, "variables": variables},
140
+ timeout=15,
141
+ )
142
+
143
+ if self._http is not None:
144
+ res = await call(self._http)
145
+ else:
146
+ async with httpx.AsyncClient() as http:
147
+ res = await call(http)
148
+ res.raise_for_status()
149
+ body = res.json()
150
+ if errors := body.get("errors"):
151
+ raise LinearApiError(errors[0].get("message", "unknown error"))
152
+ return body["data"]
@@ -0,0 +1,157 @@
1
+ """Framework-free Linear webhook dispatch: verify, parse, route.
2
+
3
+ Works with any HTTP framework (or none) — you bring the raw request body and
4
+ the signature/timestamp headers, LinearDispatcher does the rest.
5
+ linear_events.fastapi wraps this for FastAPI apps.
6
+
7
+ Routing keys are (type, action) pairs read from the payload body:
8
+
9
+ @hook.on("Issue") -> every Issue action
10
+ @hook.on("Comment", action="create") -> only new comments
11
+ @hook.on_any -> (type, action, payload) for all
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import hmac
18
+ import inspect
19
+ import json
20
+ import time
21
+ from collections.abc import Callable
22
+ from typing import Any, TypeVar
23
+
24
+ __all__ = ["LinearDispatcher", "LinearError", "SignatureVerificationError"]
25
+
26
+ Handler = TypeVar("Handler", bound=Callable[..., Any])
27
+
28
+ # Linear recommends rejecting deliveries older than a minute (replay window).
29
+ REPLAY_TOLERANCE_MS = 60 * 1000
30
+
31
+
32
+ class LinearError(Exception):
33
+ """Base class for dispatch failures."""
34
+
35
+
36
+ class SignatureVerificationError(LinearError):
37
+ """Linear-Signature was missing/stale/mismatched."""
38
+
39
+
40
+ class LinearDispatcher:
41
+ """Handler registry keyed by (type, action), with signature verification.
42
+
43
+ Handlers may be sync or async and receive the full payload; `payload["data"]`
44
+ holds the serialized entity. Verification is a no-op unless a signing
45
+ secret is configured.
46
+ """
47
+
48
+ def __init__(self, signing_secret: str | bytes | None = None) -> None:
49
+ if isinstance(signing_secret, str):
50
+ signing_secret = signing_secret.encode()
51
+ self._secret = signing_secret
52
+ self._handlers: dict[tuple[str, str | None], list[Callable[..., Any]]] = {}
53
+ self._any_handlers: list[Callable[..., Any]] = []
54
+
55
+ def on(
56
+ self, type: str, action: str | list[str] | None = None
57
+ ) -> Callable[[Handler], Handler]:
58
+ """Register a handler for one entity type, optionally narrowed to one
59
+ action or a list of actions (the handler fires for any of them)."""
60
+ actions: list[str | None]
61
+ if action is None:
62
+ actions = [None]
63
+ elif isinstance(action, str):
64
+ actions = [action]
65
+ else:
66
+ actions = list(dict.fromkeys(action)) # dedupe, keep order
67
+ if not actions:
68
+ raise ValueError("action list must not be empty")
69
+
70
+ def register(fn: Handler) -> Handler:
71
+ for a in actions:
72
+ self._handlers.setdefault((type, a), []).append(fn)
73
+ return fn
74
+
75
+ return register
76
+
77
+ def on_any(self, fn: Handler) -> Handler:
78
+ """Register a handler called with (type, action, payload) for every delivery."""
79
+ self._any_handlers.append(fn)
80
+ return fn
81
+
82
+ def verify_signature(self, body: bytes, signature: str | None) -> None:
83
+ """Linear-Signature is a plain hex HMAC-SHA256 over the raw body,
84
+ constant-time compared. No-op unless a signing secret is configured."""
85
+ if self._secret is None:
86
+ return
87
+ if not signature:
88
+ raise SignatureVerificationError("missing Linear-Signature header")
89
+ expected = hmac.new(self._secret, body, hashlib.sha256).hexdigest()
90
+ if not hmac.compare_digest(expected, signature):
91
+ raise SignatureVerificationError("Linear-Signature mismatch")
92
+
93
+ def _check_freshness(self, timestamp_ms: int | None) -> None:
94
+ """The 60-second replay window Linear recommends, from the
95
+ Linear-Timestamp header or the body's webhookTimestamp."""
96
+ if self._secret is None:
97
+ return
98
+ if timestamp_ms is None:
99
+ raise SignatureVerificationError(
100
+ "missing Linear-Timestamp header / webhookTimestamp field"
101
+ )
102
+ if abs(time.time() * 1000 - timestamp_ms) > REPLAY_TOLERANCE_MS:
103
+ raise SignatureVerificationError(
104
+ "stale Linear-Timestamp (replay protection)"
105
+ )
106
+
107
+ async def dispatch(
108
+ self,
109
+ body: bytes | str,
110
+ *,
111
+ timestamp: str | None = None,
112
+ signature: str | None = None,
113
+ ) -> dict[str, Any]:
114
+ """Verify, parse and route one delivery; returns the payload.
115
+
116
+ Raises SignatureVerificationError (a secret is configured and the
117
+ signature is missing/wrong or the timestamp is missing/stale),
118
+ json.JSONDecodeError (malformed body), LinearError (unusable payload).
119
+ """
120
+ raw = body.encode() if isinstance(body, str) else body
121
+ self.verify_signature(raw, signature)
122
+ payload = json.loads(raw)
123
+ if not isinstance(payload, dict) or "type" not in payload:
124
+ raise LinearError("malformed payload: expected a JSON object with a `type`")
125
+
126
+ timestamp_ms: int | None
127
+ if timestamp is not None:
128
+ try:
129
+ timestamp_ms = int(timestamp)
130
+ except ValueError:
131
+ raise SignatureVerificationError("malformed Linear-Timestamp header")
132
+ else:
133
+ timestamp_ms = payload.get("webhookTimestamp")
134
+ if not isinstance(timestamp_ms, int):
135
+ timestamp_ms = None
136
+ self._check_freshness(timestamp_ms)
137
+
138
+ event_type = payload["type"]
139
+ action = payload.get("action")
140
+ # dict.fromkeys dedupes (action-less events would otherwise route twice)
141
+ for key in dict.fromkeys(((event_type, action), (event_type, None))):
142
+ for fn in self._handlers.get(key, []):
143
+ if inspect.isawaitable(result := fn(payload)):
144
+ await result
145
+ for fn in self._any_handlers:
146
+ if inspect.isawaitable(result := fn(event_type, action, payload)):
147
+ await result
148
+ return payload
149
+
150
+ def as_fastapi_router(self, *, path: str = "/") -> Any:
151
+ """Build a FastAPI APIRouter with a POST endpoint wired to this dispatcher.
152
+
153
+ Requires fastapi (linear-events[fastapi]). Returns fastapi.APIRouter.
154
+ """
155
+ from linear_events.fastapi import create_router
156
+
157
+ return create_router(self, path=path)
@@ -0,0 +1,55 @@
1
+ """FastAPI integration: mount one endpoint that verifies and dispatches deliveries.
2
+
3
+ from fastapi import FastAPI
4
+ from linear_events import LinearDispatcher
5
+
6
+ hook = LinearDispatcher(signing_secret="...")
7
+ app = FastAPI()
8
+ app.include_router(hook.as_fastapi_router(path="/webhooks/linear"))
9
+
10
+ Responses: 200 on success (handled or not — Linear retries anything non-200,
11
+ so unknown types are acknowledged too), 400 for malformed payloads, 401 for
12
+ bad signatures or stale timestamps.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+
19
+ try:
20
+ from fastapi import APIRouter, HTTPException, Request, Response
21
+ except ImportError as e: # pragma: no cover
22
+ raise ImportError(
23
+ "linear_events.fastapi requires fastapi — install linear-events[fastapi]"
24
+ ) from e
25
+
26
+ from linear_events.dispatch import (
27
+ LinearDispatcher,
28
+ LinearError,
29
+ SignatureVerificationError,
30
+ )
31
+
32
+ __all__ = ["create_router"]
33
+
34
+
35
+ def create_router(dispatcher: LinearDispatcher, *, path: str = "/") -> APIRouter:
36
+ """An APIRouter whose single POST endpoint feeds deliveries to `dispatcher`."""
37
+ router = APIRouter()
38
+
39
+ @router.post(path)
40
+ async def receive_linear_event(request: Request) -> Response:
41
+ try:
42
+ await dispatcher.dispatch(
43
+ await request.body(),
44
+ timestamp=request.headers.get("linear-timestamp"),
45
+ signature=request.headers.get("linear-signature"),
46
+ )
47
+ except SignatureVerificationError as e:
48
+ raise HTTPException(401, str(e)) from e
49
+ except json.JSONDecodeError as e:
50
+ raise HTTPException(400, f"invalid JSON body: {e}") from e
51
+ except LinearError as e:
52
+ raise HTTPException(400, str(e)) from e
53
+ return Response(status_code=200)
54
+
55
+ return router
@@ -0,0 +1,140 @@
1
+ """Hand-written TypedDicts for the Linear webhook envelope.
2
+
3
+ The envelope's fields are stable and documented; `data` is the serialized
4
+ entity (Issue, Comment, ...) and moves with Linear's GraphQL schema, so it
5
+ stays a plain dict — handlers can rely on the envelope, not the entity.
6
+ """
7
+
8
+ from typing import Any, TypedDict
9
+
10
+ __all__ = [
11
+ "Actor",
12
+ "AgentActivityWebhookPayload",
13
+ "AgentSessionEventWebhookPayload",
14
+ "AgentSessionWebhookPayload",
15
+ "CommentChildWebhookPayload",
16
+ "GuidanceRuleWebhookPayload",
17
+ "IssueChildWebhookPayload",
18
+ "UserChildWebhookPayload",
19
+ "WebhookPayload",
20
+ ]
21
+
22
+
23
+ class Actor(TypedDict, total=False):
24
+ """Who triggered the action. None when the triggering user or integration
25
+ has since been deleted."""
26
+
27
+ id: str
28
+ type: str # "user" | "application" | "integration"
29
+ name: str
30
+ email: str
31
+ url: str
32
+
33
+
34
+ class WebhookPayload(TypedDict, total=False):
35
+ """One Linear delivery. Data-change events (Issue, Comment, ...) carry
36
+ action/type/data; convenience streams (Issue SLA, OAuthApp revoked) add
37
+ their own fields on top."""
38
+
39
+ action: str # "create" | "update" | "remove" for data-change events
40
+ type: str # "Issue" | "Comment" | "Project" | ... (mirrors Linear-Event)
41
+ actor: Actor | None
42
+ createdAt: str # ISO-8601
43
+ data: dict[str, Any] # the serialized subject entity
44
+ url: str # deep link to the subject entity
45
+ updatedFrom: dict[str, Any] # update actions: previous values
46
+ organizationId: str
47
+ webhookId: str
48
+ webhookTimestamp: int # ms; Linear recommends a 60s freshness window
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Agent session events (the Agent Interaction model): type is
53
+ # "AgentSessionEvent", action "created" (mention/delegation — a new agent
54
+ # loop should start) or "prompted" (a follow-up user message, in
55
+ # agentActivity.content["body"]).
56
+
57
+
58
+ class UserChildWebhookPayload(TypedDict, total=False):
59
+ id: str
60
+ name: str
61
+ displayName: str
62
+ email: str
63
+ url: str
64
+
65
+
66
+ class IssueChildWebhookPayload(TypedDict, total=False):
67
+ id: str
68
+ identifier: str # "ENG-123"
69
+ title: str
70
+ description: str
71
+ url: str
72
+ teamId: str
73
+
74
+
75
+ class CommentChildWebhookPayload(TypedDict, total=False):
76
+ id: str
77
+ body: str
78
+ userId: str
79
+ issueId: str
80
+ url: str
81
+
82
+
83
+ class GuidanceRuleWebhookPayload(TypedDict, total=False):
84
+ """Instructions configured at the workspace/team level (preferred repos,
85
+ task constraints); the nearest team-specific rule takes precedence."""
86
+
87
+ id: str
88
+ body: str
89
+ origin: str # "workspace" | "parentTeam" | "team"
90
+
91
+
92
+ class AgentSessionWebhookPayload(TypedDict, total=False):
93
+ id: str
94
+ status: str # pending | active | awaitingInput | complete | error | stale
95
+ type: str
96
+ appUserId: str
97
+ organizationId: str
98
+ issueId: str | None
99
+ issue: IssueChildWebhookPayload | None
100
+ commentId: str | None
101
+ comment: CommentChildWebhookPayload | None
102
+ creatorId: str | None # the responsible human (unset for automation)
103
+ creator: UserChildWebhookPayload | None
104
+ sourceCommentId: str | None
105
+ url: str | None
106
+ summary: str | None
107
+ startedAt: str | None
108
+ endedAt: str | None
109
+ createdAt: str
110
+ updatedAt: str
111
+
112
+
113
+ class AgentActivityWebhookPayload(TypedDict, total=False):
114
+ id: str
115
+ agentSessionId: str
116
+ content: dict[str, Any] # {"type": "prompt", "body": ...} for `prompted`
117
+ signal: str | None
118
+ signalMetadata: dict[str, Any] | None
119
+ sourceCommentId: str | None
120
+ userId: str
121
+ user: UserChildWebhookPayload
122
+ createdAt: str
123
+ updatedAt: str
124
+
125
+
126
+ class AgentSessionEventWebhookPayload(TypedDict, total=False):
127
+ action: str # "created" | "prompted"
128
+ type: str # always "AgentSessionEvent"
129
+ agentSession: AgentSessionWebhookPayload
130
+ agentActivity: AgentActivityWebhookPayload | None # set for `prompted`
131
+ appUserId: str
132
+ oauthClientId: str
133
+ organizationId: str
134
+ guidance: list[GuidanceRuleWebhookPayload] | None
135
+ previousComments: list[CommentChildWebhookPayload] | None
136
+ # Formatted context string (issue, comments, guidance); `created` only.
137
+ promptContext: str | None
138
+ webhookId: str
139
+ webhookTimestamp: int # ms
140
+ createdAt: str
File without changes