@agent-os-sdk/client 0.9.1 → 0.9.3

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 (40) hide show
  1. package/README.md +26 -102
  2. package/dist/client/AgentOsClient.d.ts +3 -3
  3. package/dist/client/AgentOsClient.d.ts.map +1 -1
  4. package/dist/client/AgentOsClient.js +4 -4
  5. package/dist/generated/openapi.d.ts +951 -519
  6. package/dist/generated/openapi.d.ts.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/modules/agents.d.ts +5 -2
  11. package/dist/modules/agents.d.ts.map +1 -1
  12. package/dist/modules/agents.js +4 -1
  13. package/dist/modules/chatwoot.d.ts +59 -0
  14. package/dist/modules/chatwoot.d.ts.map +1 -0
  15. package/dist/modules/chatwoot.js +200 -0
  16. package/dist/modules/credentials.d.ts +1 -1
  17. package/dist/modules/credentials.d.ts.map +1 -1
  18. package/dist/modules/credentials.js +5 -2
  19. package/dist/modules/runs.d.ts +40 -63
  20. package/dist/modules/runs.d.ts.map +1 -1
  21. package/dist/modules/runs.js +86 -78
  22. package/dist/modules/triggers.d.ts +21 -0
  23. package/dist/modules/triggers.d.ts.map +1 -1
  24. package/dist/sse/client.d.ts +2 -2
  25. package/dist/sse/client.js +2 -2
  26. package/package.json +2 -2
  27. package/src/client/AgentOsClient.ts +4 -4
  28. package/src/generated/openapi.ts +951 -519
  29. package/src/generated/swagger.json +1295 -474
  30. package/src/index.ts +1 -0
  31. package/src/modules/agents.ts +9 -3
  32. package/src/modules/chatwoot.ts +242 -0
  33. package/src/modules/credentials.ts +5 -2
  34. package/src/modules/runs.ts +120 -107
  35. package/src/modules/triggers.ts +21 -0
  36. package/src/sse/client.ts +2 -2
  37. package/dist/modules/mcp.d.ts +0 -39
  38. package/dist/modules/mcp.d.ts.map +0 -1
  39. package/dist/modules/mcp.js +0 -38
  40. package/src/modules/mcp.ts +0 -59
package/src/index.ts CHANGED
@@ -96,6 +96,7 @@ export type { SSEEvent, SSEOptions } from "./sse/client.js";
96
96
 
97
97
  export * from "./modules/agents.js";
98
98
  export * from "./modules/auth.js";
99
+ export * from "./modules/chatwoot.js";
99
100
  export * from "./modules/credentials.js";
100
101
  export * from "./modules/me.js";
101
102
  export * from "./modules/members.js";
@@ -7,8 +7,8 @@
7
7
  * - create*, update*, delete* for mutations
8
8
  */
9
9
 
10
- import type { RawClient, APIResponse, components } from "../client/raw.js";
11
- import type { PaginationParams, PaginatedResponse } from "../client/helpers.js";
10
+ import type { PaginatedResponse, PaginationParams } from "../client/helpers.js";
11
+ import type { APIResponse, components, RawClient } from "../client/raw.js";
12
12
 
13
13
  // Type aliases for this module
14
14
 
@@ -31,6 +31,7 @@ export interface Agent {
31
31
  last_run_id?: string | null;
32
32
  last_run_status?: string | null;
33
33
  last_run_at?: string | null;
34
+ metadata?: any | null;
34
35
  }
35
36
 
36
37
  export interface AgentBundle extends AgentBundleSchema { }
@@ -106,6 +107,7 @@ export class AgentsModule {
106
107
  */
107
108
  async create(body: {
108
109
  name: string;
110
+ metadata?: any | null;
109
111
  /** Idempotency key for safe retries. When set, duplicate requests with the same key return 409 Conflict. */
110
112
  idempotency_key?: string;
111
113
  }): Promise<APIResponse<Agent>> {
@@ -115,7 +117,10 @@ export class AgentsModule {
115
117
  }
116
118
 
117
119
  return this.client.POST<Agent>("/v1/api/agents", {
118
- body: { name: body.name },
120
+ body: {
121
+ name: body.name,
122
+ metadata: body.metadata
123
+ },
119
124
  headers,
120
125
  });
121
126
  }
@@ -127,6 +132,7 @@ export class AgentsModule {
127
132
  async update(agentId: string, body: {
128
133
  name?: string;
129
134
  live_bundle_id?: string;
135
+ metadata?: any | null;
130
136
  }): Promise<APIResponse<Agent>> {
131
137
  return this.client.PATCH<Agent>("/v1/api/agents/{id}", {
132
138
  params: { path: { id: agentId } },
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Chatwoot Module
3
+ */
4
+
5
+ import type { APIResponse, RawClient } from "../client/raw.js";
6
+
7
+ export interface ChatwootInboxUrlResponse {
8
+ url: string;
9
+ }
10
+
11
+ export interface ChatwootConfig {
12
+ baseUrl: string;
13
+ accountId: string;
14
+ apiAccessToken: string;
15
+ }
16
+
17
+ export class ChatwootModule {
18
+ constructor(
19
+ private client: RawClient,
20
+ private headers: () => Record<string, string>
21
+ ) { }
22
+
23
+ /**
24
+ * Internal helper to resolve Chatwoot configuration from AgentOS credentials
25
+ */
26
+ private async _getChatwootConfig(credentialId: string): Promise<{ data: ChatwootConfig | undefined; error: any; response?: Response }> {
27
+ const { data: credential, error } = await this.client.GET<any>("/v1/api/credentials/{id}", {
28
+ params: {
29
+ path: { id: credentialId },
30
+ query: { includeValues: true }
31
+ },
32
+ headers: this.headers(),
33
+ });
34
+
35
+ if (error || !credential) {
36
+ return { error: error || { message: "Credential not found", code: "CREDENTIAL_NOT_FOUND" }, data: undefined };
37
+ }
38
+
39
+ // Backend now returns 'values' when includeValues=true
40
+ const values = credential.values || credential.Values || {};
41
+ const publicConfig = credential.publicConfig || credential.PublicConfig || {};
42
+ const data = credential.data || {};
43
+
44
+ const url = publicConfig.url || data.url || values.url || publicConfig.endpoint || data.endpoint || values.endpoint || values.base_url;
45
+ let accountId = publicConfig.account_id || data.account_id || values.account_id || publicConfig.accountId || data.accountId || values.accountId;
46
+ const apiAccessToken = values.api_access_token || data.api_access_token || values.api_key || data.api_key || values.apiKey || data.apiKey || values.api_token;
47
+
48
+ if (!url || !apiAccessToken) {
49
+ return { error: { message: "Invalid Chatwoot credential: missing URL/Endpoint or API Access Token/Key", code: "INVALID_CREDENTIAL" }, data: undefined };
50
+ }
51
+
52
+ const baseUrl = url.endsWith("/") ? url.slice(0, -1) : url;
53
+
54
+ // Auto-discover accountId if missing
55
+ if (!accountId) {
56
+ try {
57
+ const profileRes = await fetch(`${baseUrl}/api/v1/profile`, {
58
+ headers: { "api_access_token": apiAccessToken }
59
+ });
60
+ if (profileRes.ok) {
61
+ const profile = await profileRes.json();
62
+ // Use the first available account
63
+ if (profile.accounts && profile.accounts.length > 0) {
64
+ accountId = profile.accounts[0].id;
65
+ }
66
+ }
67
+ } catch (err) {
68
+ console.warn("Failed to auto-discover Chatwoot account ID:", err);
69
+ }
70
+ }
71
+
72
+ if (!accountId) {
73
+ return { error: { message: "Invalid Chatwoot credential: missing Account ID and auto-discovery failed.", code: "ACCOUNT_ID_MISSING" }, data: undefined };
74
+ }
75
+
76
+ return {
77
+ data: {
78
+ baseUrl,
79
+ accountId: String(accountId),
80
+ apiAccessToken
81
+ },
82
+ error: null
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Direct call to Chatwoot API
88
+ */
89
+ private async _chatwootRequest(config: ChatwootConfig, method: string, path: string, body?: any): Promise<APIResponse<any>> {
90
+ const url = `${config.baseUrl}${path}`;
91
+ const headers = {
92
+ "api_access_token": config.apiAccessToken,
93
+ "Content-Type": "application/json",
94
+ };
95
+
96
+ try {
97
+ const response = await fetch(url, {
98
+ method,
99
+ headers,
100
+ body: body ? JSON.stringify(body) : undefined,
101
+ });
102
+
103
+ let data = null;
104
+ const contentType = response.headers.get("content-type");
105
+ if (contentType && contentType.includes("application/json")) {
106
+ try {
107
+ const text = await response.text();
108
+ if (text) data = JSON.parse(text);
109
+ } catch (e) {
110
+ // Ignore JSON parse errors for empty bodies
111
+ }
112
+ }
113
+
114
+ if (!response.ok) {
115
+ return { error: data || { message: `Chatwoot API error: ${response.statusText}`, code: "API_ERROR" }, data: undefined, response };
116
+ }
117
+
118
+ return { data, error: undefined, response };
119
+ } catch (err) {
120
+ return { error: { message: (err as Error).message, code: "UNKNOWN_ERROR" }, data: undefined, response: new Response() };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get the inbox URL for a specific credential.
126
+ * Use this to open the Chatwoot inbox directly.
127
+ */
128
+ async getInboxUrl(credentialId: string): Promise<APIResponse<ChatwootInboxUrlResponse>> {
129
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
130
+
131
+ if (error || !config) {
132
+ return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
133
+ }
134
+
135
+ const inboxUrl = `${config.baseUrl}/app/accounts/${config.accountId}/inbox`;
136
+
137
+ return {
138
+ data: { url: inboxUrl },
139
+ error: undefined,
140
+ response: new Response(),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * List all inboxes
146
+ */
147
+ async listInboxes(credentialId: string): Promise<APIResponse<any[]>> {
148
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
149
+ if (error || !config) return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
150
+
151
+ const res = await this._chatwootRequest(config, "GET", `/api/v1/accounts/${config.accountId}/inboxes`);
152
+
153
+ // Chatwoot API returns { payload: [...] }
154
+ if (res.data && Array.isArray(res.data.payload)) {
155
+ return { data: res.data.payload, error: undefined, response: res.response };
156
+ }
157
+
158
+ return res;
159
+ }
160
+
161
+ /**
162
+ * Create a new inbox
163
+ */
164
+ async createInbox(credentialId: string, data: any): Promise<APIResponse<any>> {
165
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
166
+ if (error || !config) return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
167
+
168
+ return this._chatwootRequest(config, "POST", `/api/v1/accounts/${config.accountId}/inboxes`, data);
169
+ }
170
+
171
+ /**
172
+ * Get a specific inbox
173
+ */
174
+ async getInbox(credentialId: string, inboxId: string | number): Promise<APIResponse<any>> {
175
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
176
+ if (error || !config) return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
177
+
178
+ return this._chatwootRequest(config, "GET", `/api/v1/accounts/${config.accountId}/inboxes/${inboxId}`);
179
+ }
180
+
181
+ /**
182
+ * Update an inbox
183
+ */
184
+ async updateInbox(credentialId: string, inboxId: string | number, data: any): Promise<APIResponse<any>> {
185
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
186
+ if (error || !config) return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
187
+
188
+ return this._chatwootRequest(config, "PATCH", `/api/v1/accounts/${config.accountId}/inboxes/${inboxId}`, data);
189
+ }
190
+
191
+ /**
192
+ * Delete an inbox
193
+ */
194
+ async deleteInbox(credentialId: string, inboxId: string | number): Promise<APIResponse<any>> {
195
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
196
+ if (error || !config) return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
197
+
198
+ return this._chatwootRequest(config, "DELETE", `/api/v1/accounts/${config.accountId}/inboxes/${inboxId}`);
199
+ }
200
+
201
+ /**
202
+ * Get inbox metrics
203
+ */
204
+ async getInboxMetrics(credentialId: string, inboxId: string | number): Promise<APIResponse<any>> {
205
+ const { data: config, error } = await this._getChatwootConfig(credentialId);
206
+ if (error || !config) return { error: error || { message: "Config not found", code: "CONFIG_MISSING" }, data: undefined, response: new Response() };
207
+
208
+ return this._chatwootRequest(config, "GET", `/api/v1/accounts/${config.accountId}/inboxes/${inboxId}/metrics`);
209
+ }
210
+
211
+ /**
212
+ * Get the inbox URL for a specific agent.
213
+ */
214
+ async getAgentInboxUrl(agentId: string): Promise<APIResponse<ChatwootInboxUrlResponse>> {
215
+ const { data: triggers, error: triggerError } = await this.client.GET<any>("/v1/api/triggers", {
216
+ params: { query: { agent_id: agentId } },
217
+ headers: this.headers(),
218
+ });
219
+
220
+ if (triggerError || !triggers) {
221
+ return { error: triggerError || { message: "Failed to fetch triggers", code: "TRIGGER_FETCH_FAILED" }, data: undefined, response: new Response() };
222
+ }
223
+
224
+ const chatwootTrigger = triggers.items?.find((t: any) =>
225
+ t.type === "chatwoot" ||
226
+ (t.type === "evolution_whatsapp" && t.config?.chat_platform === "chatwoot") ||
227
+ (t.config?.credential_id && (t.type === "chatwoot" || t.type.includes("whatsapp")))
228
+ );
229
+
230
+ if (!chatwootTrigger) {
231
+ return { error: { message: "No compatible trigger found for this agent", code: "NO_TRIGGER" }, data: undefined, response: new Response() };
232
+ }
233
+
234
+ const credentialId = chatwootTrigger.config?.credential_id;
235
+
236
+ if (!credentialId) {
237
+ return { error: { message: "Trigger configuration missing credential_id", code: "MISSING_CREDENTIAL_ID" }, data: undefined, response: new Response() };
238
+ }
239
+
240
+ return this.getInboxUrl(credentialId);
241
+ }
242
+ }
@@ -68,9 +68,12 @@ export class CredentialsModule {
68
68
  /**
69
69
  * Get a credential by ID.
70
70
  */
71
- async get(credentialId: string): Promise<APIResponse<Credential>> {
71
+ async get(credentialId: string, includeValues: boolean = false): Promise<APIResponse<Credential>> {
72
72
  return this.client.GET<Credential>("/v1/api/credentials/{id}", {
73
- params: { path: { id: credentialId } },
73
+ params: {
74
+ path: { id: credentialId },
75
+ query: { includeValues }
76
+ },
74
77
  headers: this.headers(),
75
78
  });
76
79
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  import type { RawClient, APIResponse, components } from "../client/raw.js";
11
11
  import type { PaginationParams, PaginatedResponse } from "../client/helpers.js";
12
- import { parseSSE, type SSEEvent, type RunStreamEvent, type SSEOptions } from "../sse/client.js";
12
+ import { parseSSE, type SSEEvent } from "../sse/client.js";
13
13
 
14
14
  // Type aliases from OpenAPI
15
15
  type WaitRunResponse = components["schemas"]["WaitRunResponse"];
@@ -29,6 +29,9 @@ export interface Run {
29
29
  input?: unknown;
30
30
  output?: unknown;
31
31
  error?: unknown;
32
+ current_attempt_id?: string;
33
+ current_attempt_no?: number;
34
+ latest_seq?: number;
32
35
  created_at: string;
33
36
  started_at?: string;
34
37
  completed_at?: string;
@@ -54,15 +57,6 @@ export type RunStatus =
54
57
  | "waiting_for_human"
55
58
  | "resumed";
56
59
 
57
- export interface RunEvent {
58
- id: string;
59
- run_id: string;
60
- event_type: string;
61
- node?: string;
62
- data?: unknown;
63
- timestamp: string;
64
- }
65
-
66
60
  export interface CreateRunResponse {
67
61
  run_id: string;
68
62
  status: string;
@@ -70,13 +64,12 @@ export interface CreateRunResponse {
70
64
  }
71
65
 
72
66
  export type RunListResponse = PaginatedResponse<Run>;
73
- export type RunEventsResponse = PaginatedResponse<RunEvent>;
74
67
 
75
68
  /** Wave 2.3: Seq-based polling response for execution events */
76
69
  export interface RunEventsPollResponse {
77
70
  events: RunEventDto[];
78
71
  latest_seq: number;
79
- next_after_seq: number;
72
+ next_after_seq: number | null;
80
73
  has_more: boolean;
81
74
  }
82
75
 
@@ -321,20 +314,28 @@ export class RunsModule {
321
314
  // ======================== Events ========================
322
315
 
323
316
  /**
324
- * Get run events for timeline (paged, no SSE).
325
- * @example
326
- * ```ts
327
- * const { data } = await client.runs.getEvents("run-uuid");
328
- * ```
317
+ * Get canonical run events from SSOT ledger (polling by seq).
329
318
  */
330
- async getEvents(runId: string, params?: PaginationParams): Promise<APIResponse<RunEventsResponse>> {
331
- return this.client.GET<RunEventsResponse>("/v1/api/runs/{runId}/events", {
332
- params: { path: { runId }, query: params },
319
+ async getEvents(runId: string, params: {
320
+ attemptId: string;
321
+ afterSeq?: number;
322
+ limit?: number;
323
+ }): Promise<APIResponse<RunEventsPollResponse>> {
324
+ return this.client.GET<RunEventsPollResponse>("/v1/api/runs/{runId}/events", {
325
+ params: {
326
+ path: { runId },
327
+ query: {
328
+ attemptId: params.attemptId,
329
+ afterSeq: params.afterSeq ?? 0,
330
+ limit: params.limit ?? 100,
331
+ },
332
+ },
333
333
  });
334
334
  }
335
335
 
336
336
  /** Alias: runs.events() -> runs.getEvents() */
337
- events = (runId: string, params?: PaginationParams) => this.getEvents(runId, params);
337
+ events = (runId: string, params: { attemptId: string; afterSeq?: number; limit?: number }) =>
338
+ this.getEvents(runId, params);
338
339
 
339
340
  /**
340
341
  * Wave 2.3: Seq-based event polling for execution trace.
@@ -347,8 +348,9 @@ export class RunsModule {
347
348
  * @example
348
349
  * ```ts
349
350
  * let afterSeq = 0;
351
+ * const attemptId = "attempt-uuid";
350
352
  * while (run.status === 'running') {
351
- * const { data } = await client.runs.pollEvents(runId, { afterSeq });
353
+ * const { data } = await client.runs.pollEvents(runId, { attemptId, afterSeq });
352
354
  * for (const event of data.events) {
353
355
  * console.log(event.type, event.payload);
354
356
  * }
@@ -357,16 +359,18 @@ export class RunsModule {
357
359
  * }
358
360
  * ```
359
361
  */
360
- async pollEvents(runId: string, params?: {
362
+ async pollEvents(runId: string, params: {
363
+ attemptId: string;
361
364
  afterSeq?: number;
362
365
  limit?: number;
363
366
  }): Promise<APIResponse<RunEventsPollResponse>> {
364
- return this.client.GET<RunEventsPollResponse>("/v1/api/runs/{runId}/events/stream", {
367
+ return this.client.GET<RunEventsPollResponse>("/v1/api/runs/{runId}/events", {
365
368
  params: {
366
369
  path: { runId },
367
370
  query: {
368
- afterSeq: params?.afterSeq ?? 0,
369
- limit: params?.limit ?? 100
371
+ attemptId: params.attemptId,
372
+ afterSeq: params.afterSeq ?? 0,
373
+ limit: params.limit ?? 100
370
374
  }
371
375
  },
372
376
  });
@@ -386,79 +390,19 @@ export class RunsModule {
386
390
  /** Alias: runs.checkpoints() -> runs.getCheckpoints() */
387
391
  checkpoints = (runId: string) => this.getCheckpoints(runId);
388
392
 
389
- // ======================== STREAMING ========================
390
-
391
- /**
392
- * Stream run events via SSE.
393
- * @example
394
- * ```ts
395
- * for await (const event of client.runs.stream("run-uuid")) {
396
- * console.log(event.data);
397
- * }
398
- * ```
399
- */
400
- async *stream(runId: string, options?: SSEOptions): AsyncGenerator<SSEEvent<RunStreamEvent>> {
401
- const response = await this.client.streamGet("/v1/api/runs/{runId}/stream", {
402
- params: { path: { runId } },
403
- headers: options?.headers,
404
- });
405
- yield* parseSSE<RunStreamEvent>(response, { onOpen: options?.onOpen });
406
- }
407
-
408
- /**
409
- * Create run and stream output.
410
- * @example
411
- * ```ts
412
- * for await (const event of client.runs.createAndStream({
413
- * agent_id: "...",
414
- * input: { message: "Hello" }
415
- * })) {
416
- * console.log(event);
417
- * }
418
- * ```
419
- */
420
- async *createAndStream(
421
- body: {
422
- agent_id: string;
423
- thread?: { thread_id?: string } | { new_thread: true };
424
- input?: unknown;
425
- },
426
- options?: SSEOptions
427
- ): AsyncGenerator<SSEEvent<RunStreamEvent>> {
428
- const { data, error } = await this.create(body);
429
- if (error || !data) {
430
- throw new Error(`Failed to create run: ${JSON.stringify(error)}`);
431
- }
432
-
433
- const runId = data.run_id;
434
- yield* this.stream(runId, options);
435
- }
436
-
437
- /**
438
- * Join an existing run's stream (resume watching).
439
- */
440
- async *join(runId: string, options?: SSEOptions): AsyncGenerator<SSEEvent<RunStreamEvent>> {
441
- const response = await this.client.streamGet("/v1/api/runs/{runId}/join", {
442
- params: { path: { runId } },
443
- headers: options?.headers,
444
- });
445
- yield* parseSSE<RunStreamEvent>(response, { onOpen: options?.onOpen });
446
- }
447
-
448
- // ======================== FOLLOW (Enterprise SSE) ========================
393
+ // ======================== FOLLOW (SSOT SSE) ========================
449
394
 
450
395
  /**
451
- * Follow a run's event stream with automatic reconnection and resume.
452
- *
453
- * This is the enterprise-grade streaming method that:
454
- * - Reconnects automatically on connection drops
455
- * - Resumes from last received event using Last-Event-ID
456
- * - Never duplicates or loses events
457
- * - Terminates cleanly on 'close' event
396
+ * Follow a run attempt's canonical event stream with automatic reconnection.
397
+ *
398
+ * SSOT contract:
399
+ * - Stream is keyed by (run_id, attempt_id, seq)
400
+ * - Resume is driven by `afterSeq` (monotonic)
401
+ * - No implicit attempt inference
458
402
  *
459
403
  * @example
460
404
  * ```ts
461
- * for await (const event of client.runs.follow(runId)) {
405
+ * for await (const event of client.runs.follow(runId, attemptId)) {
462
406
  * if (event.type === "run_event") {
463
407
  * console.log(event.payload);
464
408
  * } else if (event.type === "close") {
@@ -467,7 +411,11 @@ export class RunsModule {
467
411
  * }
468
412
  * ```
469
413
  */
470
- async *follow(runId: string, options?: FollowOptions): AsyncGenerator<FollowEvent, void, unknown> {
414
+ async *follow(runId: string, attemptId: string, options?: FollowOptions): AsyncGenerator<FollowEvent, void, unknown> {
415
+ if (!attemptId) {
416
+ throw new Error("attemptId is required for run event streaming");
417
+ }
418
+
471
419
  const signal = options?.signal;
472
420
  const maxReconnects = options?.maxReconnects ?? 10;
473
421
  const baseDelayMs = options?.baseDelayMs ?? 1000;
@@ -490,10 +438,13 @@ export class RunsModule {
490
438
  headers["Last-Event-ID"] = String(nextSeq - 1);
491
439
  }
492
440
 
493
- const response = await this.client.streamGet("/v1/api/runs/{runId}/stream", {
441
+ const response = await this.client.streamGet("/v1/api/runs/{runId}/events/stream", {
494
442
  params: {
495
443
  path: { runId },
496
- query: nextSeq > 0 ? { seq: nextSeq } : undefined // Also send as query for backends that support it
444
+ query: {
445
+ attemptId,
446
+ afterSeq: nextSeq > 0 ? nextSeq : 0,
447
+ },
497
448
  },
498
449
  headers,
499
450
  });
@@ -533,13 +484,6 @@ export class RunsModule {
533
484
  nextSeq = event.seq + 1;
534
485
  }
535
486
 
536
- // Check for terminal events
537
- if (event.type === "close") {
538
- receivedTerminal = true;
539
- yield event;
540
- return;
541
- }
542
-
543
487
  if (event.type === "error") {
544
488
  // Error event from server - yield but continue (might reconnect)
545
489
  yield event;
@@ -548,6 +492,17 @@ export class RunsModule {
548
492
  }
549
493
 
550
494
  yield event;
495
+
496
+ if (event.type === "run_event" && isTerminalRunEvent(event)) {
497
+ receivedTerminal = true;
498
+ yield {
499
+ type: "close",
500
+ seq: event.seq,
501
+ timestamp: event.timestamp,
502
+ payload: { terminal: true },
503
+ };
504
+ return;
505
+ }
551
506
  }
552
507
 
553
508
  // Stream ended without close event - this is unexpected EOF
@@ -582,24 +537,52 @@ export class RunsModule {
582
537
  }
583
538
  }
584
539
 
540
+ /**
541
+ * Create run and follow canonical SSOT stream for the current attempt.
542
+ */
543
+ async *createAndStream(
544
+ body: {
545
+ agent_id: string;
546
+ thread?: { thread_id?: string } | { new_thread: true };
547
+ input?: unknown;
548
+ /** Idempotency key for safe retries. */
549
+ idempotency_key?: string;
550
+ },
551
+ options?: FollowOptions
552
+ ): AsyncGenerator<FollowEvent, void, unknown> {
553
+ const { data, error } = await this.create(body);
554
+ if (error || !data) {
555
+ throw new Error(`Failed to create run: ${JSON.stringify(error)}`);
556
+ }
557
+
558
+ const runId = data.run_id;
559
+ const runResponse = await this.get(runId);
560
+ if (runResponse.error || !runResponse.data?.current_attempt_id) {
561
+ throw new Error("Run created without current_attempt_id; cannot stream SSOT events");
562
+ }
563
+
564
+ yield* this.follow(runId, runResponse.data.current_attempt_id, options);
565
+ }
566
+
585
567
  /**
586
568
  * Wait for a specific event that matches a predicate.
587
569
  *
588
570
  * @example
589
571
  * ```ts
590
572
  * // Wait for run completion
591
- * const event = await client.runs.waitFor(runId, (e) => e.type === "close", {
573
+ * const event = await client.runs.waitFor(runId, attemptId, (e) => e.type === "close", {
592
574
  * timeoutMs: 60000
593
575
  * });
594
576
  *
595
577
  * // Wait for specific node execution
596
- * const nodeEvent = await client.runs.waitFor(runId, (e) =>
578
+ * const nodeEvent = await client.runs.waitFor(runId, attemptId, (e) =>
597
579
  * e.type === "run_event" && e.payload?.node === "my_node"
598
580
  * );
599
581
  * ```
600
582
  */
601
583
  async waitFor(
602
584
  runId: string,
585
+ attemptId: string,
603
586
  predicate: (event: FollowEvent) => boolean,
604
587
  options?: { timeoutMs?: number; signal?: AbortSignal; startSeq?: number }
605
588
  ): Promise<FollowEvent> {
@@ -613,7 +596,7 @@ export class RunsModule {
613
596
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
614
597
 
615
598
  try {
616
- for await (const event of this.follow(runId, {
599
+ for await (const event of this.follow(runId, attemptId, {
617
600
  signal: controller.signal,
618
601
  startSeq: options?.startSeq,
619
602
  })) {
@@ -689,6 +672,28 @@ function narrowFollowEvent(raw: SSEEvent<unknown>): FollowEvent | null {
689
672
 
690
673
  const data = raw.data as Record<string, unknown> | null;
691
674
 
675
+ // Canonical payload from backend SSE is RunEventDto:
676
+ // { id, seq, type, timestamp, attempt_id, payload }
677
+ if (eventType === "run_event" && data) {
678
+ const seq = typeof data.seq === "number" ? data.seq : (raw.id ? parseInt(raw.id, 10) : -1);
679
+ const timestamp = typeof data.timestamp === "string" ? data.timestamp : new Date().toISOString();
680
+ const innerPayload = (typeof data.payload === "object" && data.payload !== null)
681
+ ? (data.payload as Record<string, unknown>)
682
+ : null;
683
+
684
+ return {
685
+ type: "run_event",
686
+ seq,
687
+ timestamp,
688
+ payload: {
689
+ ...(innerPayload ?? {}),
690
+ type: typeof data.type === "string" ? data.type : "UNKNOWN",
691
+ attempt_id: typeof data.attempt_id === "string" ? data.attempt_id : undefined,
692
+ },
693
+ node: typeof innerPayload?.node === "string" ? innerPayload.node : undefined,
694
+ };
695
+ }
696
+
692
697
  return {
693
698
  type: eventType as FollowEvent["type"],
694
699
  seq: typeof data?.seq === "number" ? data.seq : (raw.id ? parseInt(raw.id, 10) : -1),
@@ -698,6 +703,14 @@ function narrowFollowEvent(raw: SSEEvent<unknown>): FollowEvent | null {
698
703
  };
699
704
  }
700
705
 
706
+ function isTerminalRunEvent(event: FollowEvent): boolean {
707
+ const type = typeof event.payload?.type === "string"
708
+ ? event.payload.type.toUpperCase()
709
+ : "";
710
+
711
+ return type === "RUN_FINISHED" || type === "RUN_FAILED" || type === "RUN_CANCELLED";
712
+ }
713
+
701
714
  /** Calculate exponential backoff with jitter */
702
715
  function calculateBackoff(attempt: number, baseMs: number, maxMs: number): number {
703
716
  const exponential = baseMs * Math.pow(2, attempt - 1);