stackgen-sdk 0.1.5__tar.gz → 0.1.6__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.
Files changed (108) hide show
  1. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/PKG-INFO +2 -2
  2. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/README.md +1 -1
  3. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/__init__.py +19 -1
  4. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/_version.py +2 -2
  5. stackgen_sdk-0.1.6/src/stackgen/aiden/__init__.py +29 -0
  6. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/aiden/ask.py +6 -2
  7. stackgen_sdk-0.1.6/src/stackgen/aiden/namespace.py +170 -0
  8. stackgen_sdk-0.1.6/src/stackgen/aiden/runs.py +137 -0
  9. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/aiden/webhook.py +17 -21
  10. stackgen_sdk-0.1.6/src/stackgen/aiden/webhook_ops.py +172 -0
  11. stackgen_sdk-0.1.6/src/stackgen/aiden/workflow_builder.py +198 -0
  12. stackgen_sdk-0.1.6/src/stackgen/aiden/workflow_publish.py +211 -0
  13. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/config.py +1 -1
  14. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/.openapi-generator/FILES +45 -0
  15. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/__init__.py +30 -0
  16. stackgen_sdk-0.1.6/src/stackgen/generated/api/aiden_api.py +6054 -0
  17. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/api/sre_api.py +6 -6
  18. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/__init__.py +15 -0
  19. stackgen_sdk-0.1.6/src/stackgen/generated/models/agent_status.py +89 -0
  20. stackgen_sdk-0.1.6/src/stackgen/generated/models/execution_list_item.py +112 -0
  21. stackgen_sdk-0.1.6/src/stackgen/generated/models/register_agent_request.py +107 -0
  22. stackgen_sdk-0.1.6/src/stackgen/generated/models/register_agent_request_auto_approve_tools_inner.py +87 -0
  23. stackgen_sdk-0.1.6/src/stackgen/generated/models/register_agent_request_hitl.py +87 -0
  24. stackgen_sdk-0.1.6/src/stackgen/generated/models/session_export.py +100 -0
  25. stackgen_sdk-0.1.6/src/stackgen/generated/models/session_export_part.py +94 -0
  26. stackgen_sdk-0.1.6/src/stackgen/generated/models/session_export_part_kind.py +38 -0
  27. stackgen_sdk-0.1.6/src/stackgen/generated/models/session_list_response.py +97 -0
  28. stackgen_sdk-0.1.6/src/stackgen/generated/models/session_summary.py +119 -0
  29. stackgen_sdk-0.1.6/src/stackgen/generated/models/stage_binding_write.py +95 -0
  30. stackgen_sdk-0.1.6/src/stackgen/generated/models/terminate_session_response.py +91 -0
  31. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/trigger_webhook202_response.py +6 -2
  32. stackgen_sdk-0.1.6/src/stackgen/generated/models/workflow.py +117 -0
  33. stackgen_sdk-0.1.6/src/stackgen/generated/models/workflow_stage_write.py +93 -0
  34. stackgen_sdk-0.1.6/src/stackgen/generated/models/workflow_write.py +129 -0
  35. stackgen_sdk-0.1.5/src/stackgen/aiden/__init__.py +0 -11
  36. stackgen_sdk-0.1.5/src/stackgen/aiden/namespace.py +0 -78
  37. stackgen_sdk-0.1.5/src/stackgen/aiden/runs.py +0 -36
  38. stackgen_sdk-0.1.5/src/stackgen/generated/api/aiden_api.py +0 -2384
  39. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/.gitignore +0 -0
  40. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/pyproject.toml +0 -0
  41. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/_http.py +0 -0
  42. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/aiden/sessions.py +0 -0
  43. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/cli.py +0 -0
  44. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/client.py +0 -0
  45. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/errors.py +0 -0
  46. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/.openapi-generator/VERSION +0 -0
  47. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/.openapi-generator-ignore +0 -0
  48. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/api/__init__.py +0 -0
  49. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/api_client.py +0 -0
  50. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/api_response.py +0 -0
  51. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/configuration.py +0 -0
  52. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/exceptions.py +0 -0
  53. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_analysis_status.py +0 -0
  54. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_attention.py +0 -0
  55. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_categorization_summary.py +0 -0
  56. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_role.py +0 -0
  57. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_sort_by.py +0 -0
  58. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_status.py +0 -0
  59. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_summary.py +0 -0
  60. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_sync_run_status.py +0 -0
  61. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/alert_v1.py +0 -0
  62. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/artifact_info.py +0 -0
  63. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/ask_guild_request.py +0 -0
  64. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/ask_guild_start_response.py +0 -0
  65. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/candidate_type.py +0 -0
  66. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/dag_edge.py +0 -0
  67. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/dag_node.py +0 -0
  68. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/entity_ref.py +0 -0
  69. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/error_response.py +0 -0
  70. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/execution_meta.py +0 -0
  71. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/execution_structured_output.py +0 -0
  72. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigate_alert_request.py +0 -0
  73. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigate_response.py +0 -0
  74. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation.py +0 -0
  75. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_evidence.py +0 -0
  76. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_evidence_kind.py +0 -0
  77. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_evidence_source.py +0 -0
  78. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_hypothesis.py +0 -0
  79. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_plain_summary.py +0 -0
  80. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_prior_incident.py +0 -0
  81. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_recommended_next_step.py +0 -0
  82. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_ref.py +0 -0
  83. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_status.py +0 -0
  84. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_structured_hypothesis_entry.py +0 -0
  85. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_structured_limitation_entry.py +0 -0
  86. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_structured_rca.py +0 -0
  87. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/investigation_triage_metadata.py +0 -0
  88. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/json_error.py +0 -0
  89. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/list_alerts_response.py +0 -0
  90. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/list_investigations_response.py +0 -0
  91. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/pagination.py +0 -0
  92. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/prior_session_policy.py +0 -0
  93. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/qualified_candidate.py +0 -0
  94. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/schedule_run_status.py +0 -0
  95. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/schedule_target_type.py +0 -0
  96. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/session.py +0 -0
  97. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/session_responder_kind.py +0 -0
  98. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/signal_severity.py +0 -0
  99. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/sync_response.py +0 -0
  100. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/trace_dag_response.py +0 -0
  101. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/webhook_run.py +0 -0
  102. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/webhook_run_detail.py +0 -0
  103. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/models/webhook_run_list_response.py +0 -0
  104. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/generated/rest.py +0 -0
  105. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/sre/__init__.py +0 -0
  106. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/sre/namespace.py +0 -0
  107. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/vault/__init__.py +0 -0
  108. {stackgen_sdk-0.1.5 → stackgen_sdk-0.1.6}/src/stackgen/vault/namespace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: stackgen-sdk
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: StackGen SDK — Aiden, SRE, and Vault from one client
5
5
  Project-URL: Homepage, https://appcd-dev.github.io/stackgen-sdk/
6
6
  Project-URL: Documentation, https://appcd-dev.github.io/stackgen-sdk/
@@ -37,7 +37,7 @@ from stackgen import StackgenClient, StackgenConfig
37
37
 
38
38
  client = StackgenClient(
39
39
  StackgenConfig(
40
- base_url="https://azure-eu.cloud.stackgen.com",
40
+ base_url="https://app.stackgen.com",
41
41
  api_token="stackgen_…",
42
42
  org_id="<project-uuid>",
43
43
  webhook_token="sg_aios_…", # webhook journeys only
@@ -16,7 +16,7 @@ from stackgen import StackgenClient, StackgenConfig
16
16
 
17
17
  client = StackgenClient(
18
18
  StackgenConfig(
19
- base_url="https://azure-eu.cloud.stackgen.com",
19
+ base_url="https://app.stackgen.com",
20
20
  api_token="stackgen_…",
21
21
  org_id="<project-uuid>",
22
22
  webhook_token="sg_aios_…", # webhook journeys only
@@ -6,7 +6,7 @@ Public entrypoint::
6
6
 
7
7
  client = StackgenClient(
8
8
  StackgenConfig(
9
- base_url="https://azure-eu.cloud.stackgen.com",
9
+ base_url="https://app.stackgen.com",
10
10
  api_token="stackgen_…",
11
11
  org_id="<project-uuid>",
12
12
  )
@@ -18,14 +18,32 @@ from __future__ import annotations
18
18
  from stackgen.client import StackgenClient
19
19
  from stackgen.config import StackgenConfig
20
20
  from stackgen.errors import HttpError, NotAllowlistedError, StackgenError, TimeoutError
21
+ from stackgen.aiden.workflow_builder import (
22
+ AgentDefinition,
23
+ StageDefinition,
24
+ WorkflowBuilder,
25
+ WorkflowDefinition,
26
+ create_agent,
27
+ create_stage,
28
+ create_workflow,
29
+ )
30
+ from stackgen.aiden.workflow_publish import PublishWorkflowResult
21
31
 
22
32
  __all__ = [
33
+ "AgentDefinition",
23
34
  "HttpError",
24
35
  "NotAllowlistedError",
36
+ "PublishWorkflowResult",
25
37
  "StackgenClient",
26
38
  "StackgenConfig",
27
39
  "StackgenError",
40
+ "StageDefinition",
28
41
  "TimeoutError",
42
+ "WorkflowBuilder",
43
+ "WorkflowDefinition",
44
+ "create_agent",
45
+ "create_stage",
46
+ "create_workflow",
29
47
  ]
30
48
 
31
49
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.5'
22
- __version_tuple__ = version_tuple = (0, 1, 5)
21
+ __version__ = version = '0.1.6'
22
+ __version_tuple__ = version_tuple = (0, 1, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,29 @@
1
+ """Aiden (platform) namespace — webhooks, sessions, artifacts, Ask Guild, workflows."""
2
+
3
+ from stackgen.aiden.ask import AskArtifactResult
4
+ from stackgen.aiden.namespace import AidenNamespace
5
+ from stackgen.aiden.webhook import WebhookReportResult
6
+ from stackgen.aiden.workflow_builder import (
7
+ AgentDefinition,
8
+ StageDefinition,
9
+ WorkflowBuilder,
10
+ WorkflowDefinition,
11
+ create_agent,
12
+ create_stage,
13
+ create_workflow,
14
+ )
15
+ from stackgen.aiden.workflow_publish import PublishWorkflowResult
16
+
17
+ __all__ = [
18
+ "AgentDefinition",
19
+ "AidenNamespace",
20
+ "AskArtifactResult",
21
+ "PublishWorkflowResult",
22
+ "StageDefinition",
23
+ "WebhookReportResult",
24
+ "WorkflowBuilder",
25
+ "WorkflowDefinition",
26
+ "create_agent",
27
+ "create_stage",
28
+ "create_workflow",
29
+ ]
@@ -157,8 +157,12 @@ class AskArtifactJourney:
157
157
  raise StackgenError(f"unexpected getExecutionTrace payload: {data!r}")
158
158
  execution = data.get("execution") or {}
159
159
  status = _http.text(execution.get("status")).casefold()
160
- trace_settled = execution.get("trace_settled") is True
161
- if trace_settled and status in _TERMINAL_STATUSES:
160
+ # Prefer explicit settle when the mothership sends it. Some builds
161
+ # omit trace_settled on the thin getExecutionTrace summary; treat
162
+ # terminal status alone as done so callers are not stuck forever.
163
+ settled = execution.get("trace_settled")
164
+ ready = status in _TERMINAL_STATUSES and settled is not False
165
+ if ready:
162
166
  if status == "error":
163
167
  raise StackgenError(
164
168
  f"execution {trace_id} ended with status error"
@@ -0,0 +1,170 @@
1
+ """Aiden namespace — public methods for platform APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from stackgen.aiden.ask import AskArtifactJourney, AskArtifactResult
9
+ from stackgen.aiden.webhook import WebhookReportJourney, WebhookReportResult
10
+ from stackgen.aiden.webhook_ops import WebhookOps
11
+ from stackgen.aiden.workflow_builder import AgentDefinition, WorkflowDefinition
12
+ from stackgen.aiden.workflow_publish import PublishWorkflowResult, WorkflowPublishJourney
13
+ from stackgen.config import StackgenConfig
14
+
15
+
16
+ class AidenNamespace:
17
+ """Aiden platform APIs (sessions, webhooks, artifacts, Ask Guild, workflows)."""
18
+
19
+ def __init__(self, config: StackgenConfig) -> None:
20
+ self._config = config
21
+ self._webhook = WebhookReportJourney(config)
22
+ self._ask = AskArtifactJourney(config)
23
+ self._workflows = WorkflowPublishJourney(config)
24
+ self._webhook_ops = WebhookOps(config)
25
+
26
+ def run_webhook_and_download_report(
27
+ self,
28
+ payload: str,
29
+ *,
30
+ artifact_name: str | None = None,
31
+ output_path: str | None = None,
32
+ ) -> WebhookReportResult:
33
+ """Trigger a webhook, wait for the session artifact, download it."""
34
+ return self._webhook.run(
35
+ payload, artifact_name=artifact_name, output_path=output_path
36
+ )
37
+
38
+ def trigger_webhook(self, payload: str) -> dict[str, Any]:
39
+ """POST /guild/api/v1/webhooks/trigger (stateless; returns 202 body)."""
40
+ return self._webhook_ops.trigger_webhook(payload)
41
+
42
+ def get_webhook_run(self, webhook_id: str, invocation_id: str) -> dict[str, Any]:
43
+ """GET webhook invocation detail by invocation UUID."""
44
+ return self._webhook_ops.get_webhook_run(webhook_id, invocation_id)
45
+
46
+ def list_webhook_runs(self, webhook_id: str) -> list[dict[str, Any]]:
47
+ """List webhook invocations for a webhook id."""
48
+ return self._webhook_ops.list_webhook_runs(webhook_id)
49
+
50
+ def wait_for_webhook_session(
51
+ self,
52
+ webhook_id: str,
53
+ invocation_id: str,
54
+ *,
55
+ dispatch_run_id: str = "",
56
+ known_run_ids: frozenset[str] | None = None,
57
+ ) -> str:
58
+ """Poll webhook run detail until session_id is set."""
59
+ return self._webhook_ops.wait_for_webhook_session(
60
+ webhook_id,
61
+ invocation_id,
62
+ dispatch_run_id=dispatch_run_id,
63
+ known_run_ids=known_run_ids,
64
+ )
65
+
66
+ def get_execution_trace(self, trace_id: str) -> dict[str, Any]:
67
+ """GET /guild/api/v1/executions/{traceId}."""
68
+ return self._webhook_ops.get_execution_trace(trace_id)
69
+
70
+ def list_executions(self, **params: Any) -> list[dict[str, Any]]:
71
+ """GET /guild/api/v1/executions (optional session_id filter)."""
72
+ return self._webhook_ops.list_executions(**params)
73
+
74
+ def list_sessions(self, **params: Any) -> dict[str, Any]:
75
+ """GET /guild/api/v1/sessions (search with q / source_app)."""
76
+ return self._webhook_ops.list_sessions(**params)
77
+
78
+ def get_session(self, session_id: str) -> dict[str, Any]:
79
+ """GET /guild/api/v1/sessions/{sessionId}."""
80
+ return self._webhook_ops.get_session(session_id)
81
+
82
+ def list_session_artifacts(self, session_id: str) -> list[dict[str, Any]]:
83
+ """GET session artifact metadata list."""
84
+ return self._webhook_ops.list_session_artifacts(session_id)
85
+
86
+ def download_session_artifact(
87
+ self,
88
+ session_id: str,
89
+ artifact_name: str,
90
+ output_path: str,
91
+ ) -> Path:
92
+ """Download one session artifact to a local path."""
93
+ return self._webhook_ops.download_session_artifact(
94
+ session_id, artifact_name, output_path
95
+ )
96
+
97
+ def export_session(
98
+ self,
99
+ session_id: str,
100
+ *,
101
+ parts: str | None = None,
102
+ format: str = "json",
103
+ ) -> dict[str, Any]:
104
+ """GET /guild/api/v1/sessions/{sessionId}/export (report / notes / evidence)."""
105
+ return self._webhook_ops.export_session(session_id, parts=parts, format=format)
106
+
107
+ def terminate_session(self, session_id: str, *, reason: str | None = None) -> dict[str, Any]:
108
+ """POST /guild/api/v1/sessions/{sessionId}/terminate."""
109
+ return self._webhook_ops.terminate_session(session_id, reason=reason)
110
+
111
+ def run_ask_and_download_artifact(
112
+ self,
113
+ message: str,
114
+ *,
115
+ entity_refs: list[dict[str, str]] | None = None,
116
+ artifact_name: str | None = None,
117
+ output_path: str | None = None,
118
+ inputs: dict[str, str] | None = None,
119
+ session_id: str | None = None,
120
+ source_app: str | None = None,
121
+ source_metadata: dict[str, str] | None = None,
122
+ ) -> AskArtifactResult:
123
+ """Start Ask Guild, poll execution, wait for artifact, download it."""
124
+ return self._ask.run(
125
+ message,
126
+ entity_refs=entity_refs,
127
+ artifact_name=artifact_name,
128
+ output_path=output_path,
129
+ inputs=inputs,
130
+ session_id=session_id,
131
+ source_app=source_app,
132
+ source_metadata=source_metadata,
133
+ )
134
+
135
+ def start_ask(
136
+ self,
137
+ message: str,
138
+ *,
139
+ entity_refs: list[dict[str, str]] | None = None,
140
+ inputs: dict[str, str] | None = None,
141
+ session_id: str | None = None,
142
+ source_app: str | None = None,
143
+ source_metadata: dict[str, str] | None = None,
144
+ ) -> dict[str, Any]:
145
+ """POST /guild/api/v1/guild/ask/start (advanced callers)."""
146
+ return self._ask.start_ask(
147
+ message,
148
+ entity_refs=entity_refs,
149
+ inputs=inputs,
150
+ session_id=session_id,
151
+ source_app=source_app,
152
+ source_metadata=source_metadata,
153
+ )
154
+
155
+ def wait_for_execution(self, trace_id: str) -> dict[str, Any]:
156
+ """Poll GET /guild/api/v1/executions/{traceId} until settled."""
157
+ return self._ask.wait_for_execution(trace_id)
158
+
159
+ def publish_workflow(
160
+ self,
161
+ workflow: WorkflowDefinition,
162
+ *,
163
+ agents: list[AgentDefinition] | None = None,
164
+ approve: bool = True,
165
+ ) -> PublishWorkflowResult:
166
+ """Register/update agents and create/update a workflow, optionally approving it.
167
+
168
+ Pass a committed builder from ``create_workflow(...).then(...).commit()``.
169
+ """
170
+ return self._workflows.publish(workflow, agents=agents, approve=approve)
@@ -0,0 +1,137 @@
1
+ """Webhook run polling and selection helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from typing import Any
8
+
9
+ from stackgen import _http
10
+ from stackgen.config import StackgenConfig
11
+ from stackgen.errors import StackgenError, TimeoutError
12
+
13
+
14
+ def pick_run(
15
+ runs: list[dict[str, Any]],
16
+ dispatch_run_id: str,
17
+ known_run_ids: frozenset[str],
18
+ ) -> dict[str, Any] | None:
19
+ """Choose the webhook run that corresponds to a new trigger.
20
+
21
+ Preference order:
22
+ 1. Exact match on ``id`` / ``run_id`` to ``dispatch_run_id``
23
+ 2. First run whose id is not in ``known_run_ids`` (new since pre-trigger snapshot)
24
+ 3. If no known ids and runs exist, the first run (best-effort fallback)
25
+ """
26
+ if dispatch_run_id:
27
+ for item in runs:
28
+ if dispatch_run_id in {
29
+ _http.text(item.get("id")),
30
+ _http.text(item.get("run_id")),
31
+ }:
32
+ return item
33
+ for item in runs:
34
+ if _http.text(item.get("id")) not in known_run_ids:
35
+ return item
36
+ if not runs:
37
+ return None
38
+ if not known_run_ids:
39
+ return runs[0]
40
+ return None
41
+
42
+
43
+ class WebhookRunPoller:
44
+ """Poll webhook invocation rows until a session_id is available.
45
+
46
+ Mirrors the dual-path flow in Guild's webhook_session_artifact reference script.
47
+ """
48
+
49
+ def __init__(self, config: StackgenConfig) -> None:
50
+ self._config = config
51
+
52
+ def resolve_ids(
53
+ self, trigger: dict[str, Any]
54
+ ) -> tuple[str, str]:
55
+ """Pick webhook_id and invocation_id from the 202 body or config."""
56
+ cfg = self._config
57
+ webhook_id = _http.text(trigger.get("webhook_id")) or _http.text(cfg.webhook_id)
58
+ invocation_id = _http.text(trigger.get("invocation_id"))
59
+ if webhook_id:
60
+ return webhook_id, invocation_id
61
+ if _http.text(trigger.get("session_id")):
62
+ return "", invocation_id
63
+ raise StackgenError(
64
+ "trigger 202 did not include session_id or webhook_id; "
65
+ "set webhook_id for older motherships that only return run_id. "
66
+ f"body={json.dumps(trigger)}"
67
+ )
68
+
69
+ def get_run(self, webhook_id: str, run_id: str) -> dict[str, Any]:
70
+ """GET one webhook invocation (invocation UUID, not Temporal run_id)."""
71
+ url = (
72
+ f"{self._config.aiden_url()}/api/v1/webhooks/{webhook_id}/runs/{run_id}"
73
+ f"{_http.query(self._config.org_id)}"
74
+ )
75
+ data = _http.request(url, self._config.api_token)
76
+ if not isinstance(data, dict):
77
+ raise StackgenError(f"unexpected run payload: {data!r}")
78
+ return data
79
+
80
+ def list_runs(self, webhook_id: str) -> list[dict[str, Any]]:
81
+ """List invocations for a webhook."""
82
+ url = (
83
+ f"{self._config.aiden_url()}/api/v1/webhooks/{webhook_id}/runs"
84
+ f"{_http.query(self._config.org_id)}"
85
+ )
86
+ return _http.as_list(_http.request(url, self._config.api_token))
87
+
88
+ def snapshot_known_run_ids(self, webhook_id: str) -> frozenset[str]:
89
+ """Capture invocation ids before trigger for older mothership matching."""
90
+ return frozenset(
91
+ _http.text(item.get("id"))
92
+ for item in self.list_runs(webhook_id)
93
+ if _http.text(item.get("id"))
94
+ )
95
+
96
+ def wait_for_session(
97
+ self,
98
+ webhook_id: str,
99
+ invocation_id: str,
100
+ dispatch_run_id: str = "",
101
+ known_run_ids: frozenset[str] | None = None,
102
+ ) -> str:
103
+ """Poll the webhook invocation until Aiden attaches a session_id."""
104
+ cfg = self._config
105
+ known = known_run_ids if known_run_ids is not None else frozenset()
106
+ deadline = time.monotonic() + cfg.timeout_seconds
107
+ run_id_hint = invocation_id or dispatch_run_id
108
+ while time.monotonic() < deadline:
109
+ run: dict[str, Any] | None = None
110
+ if run_id_hint:
111
+ try:
112
+ run = self.get_run(webhook_id, run_id_hint)
113
+ except StackgenError:
114
+ run = None
115
+ if run is None:
116
+ run = pick_run(
117
+ self.list_runs(webhook_id),
118
+ dispatch_run_id,
119
+ known,
120
+ )
121
+ if run is None:
122
+ time.sleep(cfg.poll_interval_seconds)
123
+ continue
124
+ if not run_id_hint:
125
+ run_id_hint = _http.text(run.get("id")) or _http.text(run.get("run_id"))
126
+
127
+ status = _http.text(run.get("status")).casefold()
128
+ session_id = _http.text(run.get("session_id"))
129
+ if status in {"failed", "skipped"}:
130
+ raise StackgenError(
131
+ f"webhook invocation ended with status {status}: "
132
+ f"{run.get('error') or 'no error detail'}"
133
+ )
134
+ if session_id:
135
+ return session_id
136
+ time.sleep(cfg.poll_interval_seconds)
137
+ raise TimeoutError("webhook session", cfg.timeout_seconds)
@@ -2,15 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  from dataclasses import dataclass
7
6
  from pathlib import Path
8
7
  from typing import Any
9
8
 
10
9
  from stackgen import _http
11
10
  from stackgen.aiden import sessions as session_helpers
11
+ from stackgen.aiden.runs import WebhookRunPoller
12
+ from stackgen.aiden.webhook_ops import WebhookOps
12
13
  from stackgen.config import StackgenConfig
13
- from stackgen.errors import StackgenError
14
14
 
15
15
 
16
16
  @dataclass(frozen=True)
@@ -28,10 +28,14 @@ class WebhookReportJourney:
28
28
  """Trigger a webhook, wait for the session artifact, and download it.
29
29
 
30
30
  Uses ``webhook_token`` for POST trigger and ``api_token`` for poll/download.
31
+ When the 202 body includes ``session_id``, skips run polling. Otherwise polls
32
+ GET webhook run detail until ``session_id`` is available.
31
33
  """
32
34
 
33
35
  def __init__(self, config: StackgenConfig) -> None:
34
36
  self._config = config
37
+ self._ops = WebhookOps(config)
38
+ self._poller = WebhookRunPoller(config)
35
39
 
36
40
  def run(
37
41
  self,
@@ -47,14 +51,19 @@ class WebhookReportJourney:
47
51
  artifact = artifact_name or cfg.artifact_name
48
52
  dest = output_path or cfg.output_path
49
53
 
50
- trigger = self._trigger(payload)
51
- webhook_id = _http.text(trigger.get("webhook_id")) or _http.text(cfg.webhook_id)
52
- invocation_id = _http.text(trigger.get("invocation_id"))
53
- session_id = _http.text(trigger.get("session_id"))
54
+ known_run_ids = frozenset[str]()
55
+ if cfg.webhook_id:
56
+ known_run_ids = self._poller.snapshot_known_run_ids(cfg.webhook_id)
54
57
 
58
+ trigger = self._ops.trigger_webhook(payload)
59
+ webhook_id, invocation_id = self._poller.resolve_ids(trigger)
60
+ session_id = _http.text(trigger.get("session_id"))
55
61
  if not session_id:
56
- raise StackgenError(
57
- f"trigger response missing session_id; body={json.dumps(trigger)}"
62
+ session_id = self._poller.wait_for_session(
63
+ webhook_id,
64
+ invocation_id,
65
+ _http.text(trigger.get("run_id")),
66
+ known_run_ids,
58
67
  )
59
68
 
60
69
  artifacts = session_helpers.wait_for_artifact(cfg, session_id, artifact)
@@ -66,16 +75,3 @@ class WebhookReportJourney:
66
75
  artifacts=tuple(artifacts),
67
76
  output_path=path,
68
77
  )
69
-
70
- def _trigger(self, payload: str) -> dict[str, Any]:
71
- url = f"{self._config.aiden_url()}/api/v1/webhooks/trigger{_http.query(self._config.org_id)}"
72
- data = _http.request(
73
- url,
74
- self._config.webhook_token,
75
- method="POST",
76
- body=payload.encode("utf-8"),
77
- content_type="text/plain",
78
- )
79
- if not isinstance(data, dict):
80
- raise StackgenError(f"unexpected trigger payload: {data!r}")
81
- return data
@@ -0,0 +1,172 @@
1
+ """Stateless Aiden webhook and session artifact API helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.parse
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from stackgen import _http
10
+ from stackgen.aiden.runs import WebhookRunPoller
11
+ from stackgen.config import StackgenConfig
12
+ from stackgen.errors import StackgenError
13
+
14
+
15
+ def _query_string(params: dict[str, Any]) -> str:
16
+ cleaned = {k: v for k, v in params.items() if v is not None and v != ""}
17
+ if not cleaned:
18
+ return ""
19
+ return "?" + urllib.parse.urlencode(cleaned)
20
+
21
+
22
+ class WebhookOps:
23
+ """One-shot webhook trigger, run polling, execution trace, and artifact APIs."""
24
+
25
+ def __init__(self, config: StackgenConfig) -> None:
26
+ self._config = config
27
+ self._poller = WebhookRunPoller(config)
28
+
29
+ def trigger_webhook(self, payload: str) -> dict[str, Any]:
30
+ """POST /guild/api/v1/webhooks/trigger using the webhook token."""
31
+ cfg = self._config
32
+ if not cfg.webhook_token:
33
+ raise ValueError("webhook_token is required for trigger_webhook")
34
+ url = f"{cfg.aiden_url()}/api/v1/webhooks/trigger{_http.query(cfg.org_id)}"
35
+ data = _http.request(
36
+ url,
37
+ cfg.webhook_token,
38
+ method="POST",
39
+ body=payload.encode("utf-8"),
40
+ content_type="text/plain",
41
+ )
42
+ if not isinstance(data, dict):
43
+ raise StackgenError(f"unexpected trigger payload: {data!r}")
44
+ return data
45
+
46
+ def get_webhook_run(self, webhook_id: str, invocation_id: str) -> dict[str, Any]:
47
+ """GET /guild/api/v1/webhooks/{webhook_id}/runs/{invocation_id}."""
48
+ return self._poller.get_run(webhook_id, invocation_id)
49
+
50
+ def list_webhook_runs(self, webhook_id: str) -> list[dict[str, Any]]:
51
+ """GET /guild/api/v1/webhooks/{webhook_id}/runs."""
52
+ return self._poller.list_runs(webhook_id)
53
+
54
+ def wait_for_webhook_session(
55
+ self,
56
+ webhook_id: str,
57
+ invocation_id: str,
58
+ *,
59
+ dispatch_run_id: str = "",
60
+ known_run_ids: frozenset[str] | None = None,
61
+ ) -> str:
62
+ """Poll webhook run detail until session_id is set."""
63
+ return self._poller.wait_for_session(
64
+ webhook_id,
65
+ invocation_id,
66
+ dispatch_run_id,
67
+ known_run_ids,
68
+ )
69
+
70
+ def get_execution_trace(self, trace_id: str) -> dict[str, Any]:
71
+ """GET /guild/api/v1/executions/{traceId}."""
72
+ url = (
73
+ f"{self._config.aiden_url()}/api/v1/executions/{trace_id}"
74
+ f"{_http.query(self._config.org_id)}"
75
+ )
76
+ data = _http.request(url, self._config.api_token)
77
+ if not isinstance(data, dict):
78
+ raise StackgenError(f"unexpected execution trace payload: {data!r}")
79
+ return data
80
+
81
+ def list_executions(self, **params: Any) -> list[dict[str, Any]]:
82
+ """GET /guild/api/v1/executions (optional session_id, agent_name, limit, offset)."""
83
+ query = dict(params)
84
+ if self._config.org_id and "orgId" not in query:
85
+ query["orgId"] = self._config.org_id
86
+ url = f"{self._config.aiden_url()}/api/v1/executions{_query_string(query)}"
87
+ return _http.as_list(_http.request(url, self._config.api_token))
88
+
89
+ def list_sessions(self, **params: Any) -> dict[str, Any]:
90
+ """GET /guild/api/v1/sessions (optional q, source_app, limit, offset)."""
91
+ query = dict(params)
92
+ if self._config.org_id and "orgId" not in query:
93
+ query["orgId"] = self._config.org_id
94
+ url = f"{self._config.aiden_url()}/api/v1/sessions{_query_string(query)}"
95
+ data = _http.request(url, self._config.api_token)
96
+ if not isinstance(data, dict):
97
+ raise StackgenError(f"unexpected session list payload: {data!r}")
98
+ return data
99
+
100
+ def get_session(self, session_id: str) -> dict[str, Any]:
101
+ """GET /guild/api/v1/sessions/{sessionId}."""
102
+ url = (
103
+ f"{self._config.aiden_url()}/api/v1/sessions/{session_id}"
104
+ f"{_http.query(self._config.org_id)}"
105
+ )
106
+ data = _http.request(url, self._config.api_token)
107
+ if not isinstance(data, dict):
108
+ raise StackgenError(f"unexpected get session payload: {data!r}")
109
+ return data
110
+
111
+ def list_session_artifacts(self, session_id: str) -> list[dict[str, Any]]:
112
+ """GET /guild/api/v1/sessions/{sessionId}/artifacts."""
113
+ url = (
114
+ f"{self._config.aiden_url()}/api/v1/sessions/{session_id}/artifacts"
115
+ f"{_http.query(self._config.org_id)}"
116
+ )
117
+ return _http.as_list(_http.request(url, self._config.api_token))
118
+
119
+ def download_session_artifact(
120
+ self,
121
+ session_id: str,
122
+ artifact_name: str,
123
+ output_path: str,
124
+ ) -> Path:
125
+ """GET /guild/api/v1/sessions/{sessionId}/artifacts/{name}/download."""
126
+ encoded = urllib.parse.quote(artifact_name, safe="")
127
+ url = (
128
+ f"{self._config.aiden_url()}/api/v1/sessions/{session_id}/artifacts/"
129
+ f"{encoded}/download{_http.query(self._config.org_id)}"
130
+ )
131
+ raw = _http.request(url, self._config.api_token, expect_json=False)
132
+ dest = Path(output_path)
133
+ dest.write_bytes(raw)
134
+ return dest
135
+
136
+ def export_session(
137
+ self,
138
+ session_id: str,
139
+ *,
140
+ parts: str | None = None,
141
+ format: str = "json",
142
+ ) -> dict[str, Any]:
143
+ """GET /guild/api/v1/sessions/{sessionId}/export."""
144
+ query: dict[str, Any] = {"format": format}
145
+ if parts:
146
+ query["parts"] = parts
147
+ if self._config.org_id:
148
+ query["orgId"] = self._config.org_id
149
+ url = (
150
+ f"{self._config.aiden_url()}/api/v1/sessions/{session_id}/export"
151
+ f"{_query_string(query)}"
152
+ )
153
+ data = _http.request(url, self._config.api_token)
154
+ if not isinstance(data, dict):
155
+ raise StackgenError(f"unexpected session export payload: {data!r}")
156
+ return data
157
+
158
+ def terminate_session(self, session_id: str, *, reason: str | None = None) -> dict[str, Any]:
159
+ """POST /guild/api/v1/sessions/{sessionId}/terminate."""
160
+ query: dict[str, Any] = {}
161
+ if self._config.org_id:
162
+ query["orgId"] = self._config.org_id
163
+ if reason:
164
+ query["reason"] = reason
165
+ url = (
166
+ f"{self._config.aiden_url()}/api/v1/sessions/{session_id}/terminate"
167
+ f"{_query_string(query)}"
168
+ )
169
+ data = _http.request(url, self._config.api_token, method="POST")
170
+ if not isinstance(data, dict):
171
+ raise StackgenError(f"unexpected terminate session payload: {data!r}")
172
+ return data