|
2 | 2 |
|
3 | 3 | import type { ZodType } from 'zod';
|
4 | 4 |
|
5 |
| -import { APIResource } from '../core/resource'; |
6 |
| -import * as TasksAPI from './tasks'; |
7 | 5 | import { APIPromise } from '../core/api-promise';
|
| 6 | +import { APIResource } from '../core/resource'; |
8 | 7 | import { RequestOptions } from '../internal/request-options';
|
9 | 8 | import { path } from '../internal/utils/path';
|
10 | 9 | import {
|
11 | 10 | parseStructuredTaskOutput,
|
12 | 11 | stringifyStructuredOutput,
|
13 |
| - type TaskViewWithSchema, |
14 | 12 | type TaskCreateParamsWithSchema,
|
| 13 | + type TaskViewWithSchema, |
15 | 14 | } from '../lib/parse';
|
| 15 | +import { BrowserState, reducer } from '../lib/stream'; |
| 16 | +import * as TasksAPI from './tasks'; |
16 | 17 |
|
17 | 18 | export class Tasks extends APIResource {
|
18 | 19 | /**
|
@@ -90,6 +91,80 @@ export class Tasks extends APIResource {
|
90 | 91 | return this._client.post('/tasks', { body, ...options });
|
91 | 92 | }
|
92 | 93 |
|
| 94 | + private async *watch( |
| 95 | + data: TaskCreateParams, |
| 96 | + config: { interval: number }, |
| 97 | + options?: RequestOptions, |
| 98 | + ): AsyncGenerator<{ event: 'status'; data: TaskView }> { |
| 99 | + const tick: { current: number } = { current: 0 }; |
| 100 | + const state: { current: BrowserState } = { current: null }; |
| 101 | + |
| 102 | + poll: do { |
| 103 | + if (options?.signal?.aborted) { |
| 104 | + break poll; |
| 105 | + } |
| 106 | + |
| 107 | + tick.current++; |
| 108 | + |
| 109 | + let status: TaskView; |
| 110 | + |
| 111 | + // NOTE: We take action on each tick. |
| 112 | + if (state.current == null) { |
| 113 | + status = await this.create(data, options); |
| 114 | + } else { |
| 115 | + status = await this.retrieve(state.current.taskId); |
| 116 | + } |
| 117 | + |
| 118 | + const [newState, event] = reducer(state.current, { kind: 'status', status }); |
| 119 | + |
| 120 | + if (event != null) { |
| 121 | + yield { event: 'status', data: event }; |
| 122 | + |
| 123 | + if (event.status === 'finished') { |
| 124 | + break; |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + state.current = newState; |
| 129 | + |
| 130 | + await new Promise((resolve) => setTimeout(resolve, config.interval)); |
| 131 | + } while (true); |
| 132 | + } |
| 133 | + |
| 134 | + stream(body: TaskCreateParams, options?: RequestOptions) { |
| 135 | + const self = this; |
| 136 | + |
| 137 | + const enc = new TextEncoder(); |
| 138 | + |
| 139 | + const stream = new ReadableStream<Uint8Array>({ |
| 140 | + async start(controller) { |
| 141 | + // open the SSE stream quickly |
| 142 | + controller.enqueue(enc.encode(': connected\n\n')); |
| 143 | + |
| 144 | + try { |
| 145 | + for await (const msg of self.watch(body, { interval: 500 }, options)) { |
| 146 | + if (options?.signal?.aborted) { |
| 147 | + break; |
| 148 | + } |
| 149 | + |
| 150 | + const data = JSON.stringify(msg.data); |
| 151 | + |
| 152 | + const payload = `event: ${msg.event}\ndata: ${data}\n\n`; |
| 153 | + controller.enqueue(enc.encode(payload)); |
| 154 | + } |
| 155 | + |
| 156 | + controller.enqueue(enc.encode('event: end\ndata: {}\n\n')); |
| 157 | + } catch (e) { |
| 158 | + controller.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: String(e) })}\n\n`)); |
| 159 | + } finally { |
| 160 | + controller.close(); |
| 161 | + } |
| 162 | + }, |
| 163 | + }); |
| 164 | + |
| 165 | + return stream; |
| 166 | + } |
| 167 | + |
93 | 168 | /**
|
94 | 169 | * Get detailed information about a specific AI agent task.
|
95 | 170 | *
|
|
0 commit comments