grokbuildstreaming 0.1.0__py3-none-any.whl
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,27 @@
|
|
|
1
|
+
"""Stream text from the signed-in Grok Build CLI.
|
|
2
|
+
|
|
3
|
+
The CLI session created by ``grok login`` is the credential. This package
|
|
4
|
+
does not read or send an API key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from grokbuildstreaming.stream import (
|
|
8
|
+
DEFAULT_MODEL,
|
|
9
|
+
GrokBuildError,
|
|
10
|
+
collect_text,
|
|
11
|
+
find_grok_bin,
|
|
12
|
+
format_prompt,
|
|
13
|
+
stream_text,
|
|
14
|
+
subscription_env,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DEFAULT_MODEL",
|
|
21
|
+
"GrokBuildError",
|
|
22
|
+
"collect_text",
|
|
23
|
+
"find_grok_bin",
|
|
24
|
+
"format_prompt",
|
|
25
|
+
"stream_text",
|
|
26
|
+
"subscription_env",
|
|
27
|
+
]
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Headless Grok Build text streaming.
|
|
2
|
+
|
|
3
|
+
Each call starts one ``grok`` process and yields ``text`` events from
|
|
4
|
+
``--output-format streaming-json``. The process uses the signed-in
|
|
5
|
+
subscription session. ``XAI_API_KEY`` is removed from the child environment
|
|
6
|
+
so an exported API key is not the credential.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import signal
|
|
16
|
+
import tempfile
|
|
17
|
+
from collections.abc import Mapping, Sequence
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
DEFAULT_MODEL = "grok-4.7"
|
|
21
|
+
STREAM_LIMIT = 8 * 1024 * 1024
|
|
22
|
+
# A per-model api_key in ~/.grok/config.toml still outranks the login session.
|
|
23
|
+
# These variables are the environment fallback and are removed on purpose.
|
|
24
|
+
_API_KEY_ENV = ("XAI_API_KEY", "XAI_API_KEY_FILE")
|
|
25
|
+
_TEXT_ONLY_RULE = (
|
|
26
|
+
"Reply with the answer text only. "
|
|
27
|
+
"Do not call tools, read or edit files, browse the web, or run commands."
|
|
28
|
+
)
|
|
29
|
+
# Internal tool ids. Both shell names appear across CLI versions.
|
|
30
|
+
_DISALLOWED_TOOLS = ",".join((
|
|
31
|
+
"run_terminal_command",
|
|
32
|
+
"run_terminal_cmd",
|
|
33
|
+
"read_file",
|
|
34
|
+
"search_replace",
|
|
35
|
+
"write",
|
|
36
|
+
"list_dir",
|
|
37
|
+
"grep",
|
|
38
|
+
"web_search",
|
|
39
|
+
"web_fetch",
|
|
40
|
+
"todo_write",
|
|
41
|
+
"spawn_subagent",
|
|
42
|
+
"kill_command_or_subagent",
|
|
43
|
+
"get_command_or_subagent_output",
|
|
44
|
+
"scheduler_create",
|
|
45
|
+
"scheduler_delete",
|
|
46
|
+
"scheduler_list",
|
|
47
|
+
"monitor",
|
|
48
|
+
"search_tool",
|
|
49
|
+
"use_tool",
|
|
50
|
+
"workflow",
|
|
51
|
+
"enter_plan_mode",
|
|
52
|
+
"exit_plan_mode",
|
|
53
|
+
"ask_user_question",
|
|
54
|
+
"image_gen",
|
|
55
|
+
"image_edit",
|
|
56
|
+
"image_to_video",
|
|
57
|
+
"reference_to_video",
|
|
58
|
+
"send_feedback",
|
|
59
|
+
"Agent",
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GrokBuildError(RuntimeError):
|
|
64
|
+
"""The Grok Build CLI is missing, or a turn failed before any text."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_grok_bin() -> str:
|
|
68
|
+
"""Return the ``grok`` executable from ``PATH`` or ``~/.grok/bin/grok``."""
|
|
69
|
+
found = shutil.which("grok")
|
|
70
|
+
if found and os.path.isfile(found) and os.access(found, os.X_OK):
|
|
71
|
+
return found
|
|
72
|
+
fallback = os.path.expanduser("~/.grok/bin/grok")
|
|
73
|
+
if os.path.isfile(fallback) and os.access(fallback, os.X_OK):
|
|
74
|
+
return fallback
|
|
75
|
+
raise GrokBuildError(
|
|
76
|
+
"The grok CLI was not found. Install Grok Build, then sign in with: grok login"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def subscription_env(base: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
81
|
+
"""Environment for a child process that keeps the ``grok login`` session."""
|
|
82
|
+
env = dict(os.environ if base is None else base)
|
|
83
|
+
for name in _API_KEY_ENV:
|
|
84
|
+
env.pop(name, None)
|
|
85
|
+
env["GROK_DISABLE_AUTOUPDATER"] = "1"
|
|
86
|
+
return env
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _content_to_text(content: Any) -> str:
|
|
90
|
+
if content is None:
|
|
91
|
+
return ""
|
|
92
|
+
if isinstance(content, str):
|
|
93
|
+
return content.strip()
|
|
94
|
+
if isinstance(content, list):
|
|
95
|
+
parts: list[str] = []
|
|
96
|
+
for item in content:
|
|
97
|
+
if isinstance(item, str):
|
|
98
|
+
parts.append(item)
|
|
99
|
+
elif isinstance(item, dict):
|
|
100
|
+
text = item.get("text")
|
|
101
|
+
if not isinstance(text, str):
|
|
102
|
+
text = item.get("content")
|
|
103
|
+
if isinstance(text, str):
|
|
104
|
+
parts.append(text)
|
|
105
|
+
return "\n".join(part for part in parts if part).strip()
|
|
106
|
+
return str(content).strip()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def format_prompt(
|
|
110
|
+
messages: Sequence[Mapping[str, Any]] | None = None,
|
|
111
|
+
follow_up: str | None = None,
|
|
112
|
+
system: str | None = None,
|
|
113
|
+
) -> tuple[str, str]:
|
|
114
|
+
"""Split chat messages into a user prompt and a system prompt.
|
|
115
|
+
|
|
116
|
+
A lone user turn is left unlabeled so the model sees that text exactly.
|
|
117
|
+
Earlier turns are labeled when history is present. System messages are
|
|
118
|
+
not copied into the user prompt.
|
|
119
|
+
"""
|
|
120
|
+
system_parts: list[str] = []
|
|
121
|
+
if isinstance(system, str) and system.strip():
|
|
122
|
+
system_parts.append(system.strip())
|
|
123
|
+
turns: list[tuple[str, str]] = []
|
|
124
|
+
for message in messages or ():
|
|
125
|
+
role = str(message.get("role") or "user").strip().lower()
|
|
126
|
+
content = _content_to_text(message.get("content"))
|
|
127
|
+
if not content:
|
|
128
|
+
continue
|
|
129
|
+
if role == "system":
|
|
130
|
+
system_parts.append(content)
|
|
131
|
+
elif role == "assistant":
|
|
132
|
+
turns.append(("Assistant", content))
|
|
133
|
+
else:
|
|
134
|
+
turns.append(("User", content))
|
|
135
|
+
if isinstance(follow_up, str) and follow_up.strip():
|
|
136
|
+
turns.append(("User", follow_up.strip()))
|
|
137
|
+
system_parts.append(_TEXT_ONLY_RULE)
|
|
138
|
+
if not turns:
|
|
139
|
+
raise GrokBuildError("No prompt to send.")
|
|
140
|
+
if len(turns) == 1 and turns[0][0] == "User":
|
|
141
|
+
prompt = turns[0][1]
|
|
142
|
+
else:
|
|
143
|
+
prompt = "\n\n".join(f"{role}:\n{content}" for role, content in turns)
|
|
144
|
+
return prompt, "\n\n".join(system_parts)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_command(
|
|
148
|
+
grok_bin: str,
|
|
149
|
+
prompt_path: str,
|
|
150
|
+
work_dir: str,
|
|
151
|
+
system_prompt: str,
|
|
152
|
+
model: str,
|
|
153
|
+
max_turns: int = 1,
|
|
154
|
+
) -> list[str]:
|
|
155
|
+
"""Argv for one text-only headless turn. No API key and no tool approval."""
|
|
156
|
+
return [
|
|
157
|
+
grok_bin,
|
|
158
|
+
"--prompt-file", prompt_path,
|
|
159
|
+
"--output-format", "streaming-json",
|
|
160
|
+
"--cwd", work_dir,
|
|
161
|
+
"-m", model,
|
|
162
|
+
"--max-turns", str(max_turns),
|
|
163
|
+
"--no-plan",
|
|
164
|
+
"--no-subagents",
|
|
165
|
+
"--no-auto-update",
|
|
166
|
+
"--verbatim",
|
|
167
|
+
"--disable-web-search",
|
|
168
|
+
"--permission-mode", "dontAsk",
|
|
169
|
+
"--disallowed-tools", _DISALLOWED_TOOLS,
|
|
170
|
+
"--system-prompt-override", system_prompt,
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def parse_stream_line(line: str) -> dict[str, Any] | None:
|
|
175
|
+
"""Parse one stdout line from ``--output-format streaming-json``."""
|
|
176
|
+
text = line.strip()
|
|
177
|
+
if not text:
|
|
178
|
+
return None
|
|
179
|
+
try:
|
|
180
|
+
event = json.loads(text)
|
|
181
|
+
except json.JSONDecodeError:
|
|
182
|
+
return None
|
|
183
|
+
if isinstance(event, dict):
|
|
184
|
+
return event
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def text_from_event(event: Mapping[str, Any]) -> str | None:
|
|
189
|
+
"""Return a response text chunk. Other event types contribute no answer text."""
|
|
190
|
+
if event.get("type") != "text":
|
|
191
|
+
return None
|
|
192
|
+
data = event.get("data")
|
|
193
|
+
if isinstance(data, str) and data:
|
|
194
|
+
return data
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _completion_workspace() -> str:
|
|
199
|
+
"""Stable directory outside the caller's project, with no git root of its own."""
|
|
200
|
+
path = os.path.join(tempfile.gettempdir(), "grokbuildstreaming-workspace")
|
|
201
|
+
os.makedirs(path, exist_ok=True)
|
|
202
|
+
return path
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def _cancel_task(task: asyncio.Task | None) -> None:
|
|
206
|
+
if task is None or task.done():
|
|
207
|
+
return
|
|
208
|
+
task.cancel()
|
|
209
|
+
try:
|
|
210
|
+
await task
|
|
211
|
+
except (asyncio.CancelledError, Exception):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def _next_line(stream: asyncio.StreamReader, cancel_event: asyncio.Event | None) -> bytes | None:
|
|
216
|
+
"""Read one stdout line. ``None`` means the caller cancelled."""
|
|
217
|
+
read_task = asyncio.create_task(stream.readline())
|
|
218
|
+
if cancel_event is None:
|
|
219
|
+
return await read_task
|
|
220
|
+
if cancel_event.is_set():
|
|
221
|
+
await _cancel_task(read_task)
|
|
222
|
+
return None
|
|
223
|
+
cancel_task = asyncio.create_task(cancel_event.wait())
|
|
224
|
+
try:
|
|
225
|
+
done, _pending = await asyncio.wait(
|
|
226
|
+
{read_task, cancel_task},
|
|
227
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
228
|
+
)
|
|
229
|
+
except asyncio.CancelledError:
|
|
230
|
+
await _cancel_task(read_task)
|
|
231
|
+
await _cancel_task(cancel_task)
|
|
232
|
+
raise
|
|
233
|
+
if read_task in done and not read_task.cancelled():
|
|
234
|
+
await _cancel_task(cancel_task)
|
|
235
|
+
return read_task.result()
|
|
236
|
+
await _cancel_task(read_task)
|
|
237
|
+
await _cancel_task(cancel_task)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def _read_stderr(stream: asyncio.StreamReader | None) -> str:
|
|
242
|
+
if stream is None:
|
|
243
|
+
return ""
|
|
244
|
+
chunks: list[str] = []
|
|
245
|
+
size = 0
|
|
246
|
+
try:
|
|
247
|
+
while True:
|
|
248
|
+
line = await stream.readline()
|
|
249
|
+
if not line:
|
|
250
|
+
break
|
|
251
|
+
text = line.decode("utf-8", errors="replace")
|
|
252
|
+
chunks.append(text)
|
|
253
|
+
size += len(text)
|
|
254
|
+
if size > 16000:
|
|
255
|
+
chunks = chunks[-20:]
|
|
256
|
+
size = sum(len(item) for item in chunks)
|
|
257
|
+
except asyncio.CancelledError:
|
|
258
|
+
return "".join(chunks)[-8000:]
|
|
259
|
+
return "".join(chunks)[-8000:]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def _stop_process(process: asyncio.subprocess.Process | None) -> None:
|
|
263
|
+
if process is None or process.returncode is not None:
|
|
264
|
+
return
|
|
265
|
+
if os.name == "nt":
|
|
266
|
+
process.terminate()
|
|
267
|
+
else:
|
|
268
|
+
try:
|
|
269
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
270
|
+
except ProcessLookupError:
|
|
271
|
+
return
|
|
272
|
+
except Exception:
|
|
273
|
+
process.terminate()
|
|
274
|
+
try:
|
|
275
|
+
await asyncio.wait_for(process.wait(), timeout=3)
|
|
276
|
+
except asyncio.TimeoutError:
|
|
277
|
+
if os.name == "nt":
|
|
278
|
+
process.kill()
|
|
279
|
+
else:
|
|
280
|
+
try:
|
|
281
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
282
|
+
except ProcessLookupError:
|
|
283
|
+
return
|
|
284
|
+
except Exception:
|
|
285
|
+
process.kill()
|
|
286
|
+
try:
|
|
287
|
+
await process.wait()
|
|
288
|
+
except Exception:
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def stream_text(
|
|
293
|
+
messages: Sequence[Mapping[str, Any]] | None = None,
|
|
294
|
+
follow_up: str | None = None,
|
|
295
|
+
system: str | None = None,
|
|
296
|
+
model: str | None = None,
|
|
297
|
+
grok_bin: str | None = None,
|
|
298
|
+
cancel_event: asyncio.Event | None = None,
|
|
299
|
+
max_turns: int = 1,
|
|
300
|
+
):
|
|
301
|
+
"""Yield text chunks from one signed-in Grok Build turn.
|
|
302
|
+
|
|
303
|
+
Closing the generator, or setting ``cancel_event``, stops the CLI process.
|
|
304
|
+
``model`` defaults to :data:`DEFAULT_MODEL`. The working directory is a
|
|
305
|
+
temporary folder, and this call does not resume an earlier session.
|
|
306
|
+
"""
|
|
307
|
+
prompt, system_prompt = format_prompt(messages, follow_up=follow_up, system=system)
|
|
308
|
+
executable = grok_bin or find_grok_bin()
|
|
309
|
+
chosen_model = model.strip() if isinstance(model, str) and model.strip() else DEFAULT_MODEL
|
|
310
|
+
work_dir = _completion_workspace()
|
|
311
|
+
prompt_handle = tempfile.NamedTemporaryFile(
|
|
312
|
+
mode="w",
|
|
313
|
+
encoding="utf-8",
|
|
314
|
+
prefix="grokbuildstreaming-",
|
|
315
|
+
suffix=".txt",
|
|
316
|
+
delete=False,
|
|
317
|
+
)
|
|
318
|
+
prompt_path = prompt_handle.name
|
|
319
|
+
process: asyncio.subprocess.Process | None = None
|
|
320
|
+
stderr_task: asyncio.Task | None = None
|
|
321
|
+
produced_text = False
|
|
322
|
+
try:
|
|
323
|
+
prompt_handle.write(prompt)
|
|
324
|
+
prompt_handle.close()
|
|
325
|
+
command = build_command(
|
|
326
|
+
executable,
|
|
327
|
+
prompt_path,
|
|
328
|
+
work_dir,
|
|
329
|
+
system_prompt,
|
|
330
|
+
chosen_model,
|
|
331
|
+
max_turns=max_turns,
|
|
332
|
+
)
|
|
333
|
+
process = await asyncio.create_subprocess_exec(
|
|
334
|
+
*command,
|
|
335
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
336
|
+
stdout=asyncio.subprocess.PIPE,
|
|
337
|
+
stderr=asyncio.subprocess.PIPE,
|
|
338
|
+
cwd=work_dir,
|
|
339
|
+
env=subscription_env(),
|
|
340
|
+
start_new_session=True,
|
|
341
|
+
limit=STREAM_LIMIT,
|
|
342
|
+
)
|
|
343
|
+
if process.stdout is None:
|
|
344
|
+
raise GrokBuildError("grok did not provide an output stream")
|
|
345
|
+
stderr_task = asyncio.create_task(_read_stderr(process.stderr))
|
|
346
|
+
while True:
|
|
347
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
348
|
+
return
|
|
349
|
+
line = await _next_line(process.stdout, cancel_event)
|
|
350
|
+
if line is None:
|
|
351
|
+
return
|
|
352
|
+
if line == b"":
|
|
353
|
+
break
|
|
354
|
+
event = parse_stream_line(line.decode("utf-8", errors="replace"))
|
|
355
|
+
if not event:
|
|
356
|
+
continue
|
|
357
|
+
if event.get("type") == "error":
|
|
358
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
359
|
+
return
|
|
360
|
+
message = event.get("message") or "Grok Build reported an error."
|
|
361
|
+
raise GrokBuildError(str(message))
|
|
362
|
+
chunk = text_from_event(event)
|
|
363
|
+
if chunk:
|
|
364
|
+
produced_text = True
|
|
365
|
+
yield chunk
|
|
366
|
+
returncode = await process.wait()
|
|
367
|
+
stderr_text = ""
|
|
368
|
+
if stderr_task is not None:
|
|
369
|
+
stderr_text = (await stderr_task).strip()
|
|
370
|
+
stderr_task = None
|
|
371
|
+
if returncode not in (0, None) and not produced_text:
|
|
372
|
+
detail = stderr_text or f"grok exited with code {returncode}"
|
|
373
|
+
raise GrokBuildError(detail)
|
|
374
|
+
finally:
|
|
375
|
+
await _stop_process(process)
|
|
376
|
+
if stderr_task is not None and not stderr_task.done():
|
|
377
|
+
stderr_task.cancel()
|
|
378
|
+
try:
|
|
379
|
+
await stderr_task
|
|
380
|
+
except (asyncio.CancelledError, Exception):
|
|
381
|
+
pass
|
|
382
|
+
try:
|
|
383
|
+
os.remove(prompt_path)
|
|
384
|
+
except OSError:
|
|
385
|
+
pass
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def collect_text(
|
|
389
|
+
messages: Sequence[Mapping[str, Any]] | None = None,
|
|
390
|
+
follow_up: str | None = None,
|
|
391
|
+
system: str | None = None,
|
|
392
|
+
model: str | None = None,
|
|
393
|
+
grok_bin: str | None = None,
|
|
394
|
+
cancel_event: asyncio.Event | None = None,
|
|
395
|
+
max_turns: int = 1,
|
|
396
|
+
) -> str:
|
|
397
|
+
"""Run :func:`stream_text` and return the joined text."""
|
|
398
|
+
chunks: list[str] = []
|
|
399
|
+
async for chunk in stream_text(
|
|
400
|
+
messages,
|
|
401
|
+
follow_up=follow_up,
|
|
402
|
+
system=system,
|
|
403
|
+
model=model,
|
|
404
|
+
grok_bin=grok_bin,
|
|
405
|
+
cancel_event=cancel_event,
|
|
406
|
+
max_turns=max_turns,
|
|
407
|
+
):
|
|
408
|
+
chunks.append(chunk)
|
|
409
|
+
return "".join(chunks)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grokbuildstreaming
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stream text from the signed-in Grok Build CLI
|
|
5
|
+
Author: Eliran Wong
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# grokbuildstreaming
|
|
10
|
+
|
|
11
|
+
Stream text from the [Grok Build](https://x.ai) CLI. This library is a text-completion alternative to [agentmake](https://github.com/eliranwong/agentmake): you pass chat messages, and it yields response text as the CLI produces it.
|
|
12
|
+
|
|
13
|
+
Authentication is the subscription session from `grok login` (`~/.grok/auth.json`). The child process does not receive `XAI_API_KEY`. No API key is read or sent.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e /path/to/grokbuildstreaming
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires the `grok` executable on `PATH` or at `~/.grok/bin/grok`, and a completed `grok login`.
|
|
22
|
+
|
|
23
|
+
## Stream
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import asyncio
|
|
27
|
+
from grokbuildstreaming import stream_text
|
|
28
|
+
|
|
29
|
+
async def main():
|
|
30
|
+
messages = [
|
|
31
|
+
{"role": "system", "content": "You are a concise Bible study assistant."},
|
|
32
|
+
{"role": "user", "content": "What is John 3:16 about?"},
|
|
33
|
+
{"role": "assistant", "content": "It states God's love for the world."},
|
|
34
|
+
]
|
|
35
|
+
async for chunk in stream_text(messages, follow_up="Quote it briefly.", model="grok-4.7"):
|
|
36
|
+
print(chunk, end="", flush=True)
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`messages` use `role` and `content`, the same shape agentmake accepts. `follow_up` is the newest user turn. System text is sent with `--system-prompt-override`. Each call is a new headless process: the library does not resume a previous Grok session.
|
|
42
|
+
|
|
43
|
+
The process is text-only. It runs with `--max-turns 1`, plan mode off, web search off, subagents off, and built-in tools removed, in a temporary working directory. Cancel an in-flight stream by setting the `asyncio.Event` passed as `cancel_event`.
|
|
44
|
+
|
|
45
|
+
`collect_text` returns the whole reply:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from grokbuildstreaming import collect_text
|
|
49
|
+
|
|
50
|
+
text = await collect_text(follow_up="Reply with exactly: pong")
|
|
51
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
grokbuildstreaming/__init__.py,sha256=b7IVM4mUebgVc3gk1XWGMQwujIC9rdxQp7yqzSynKN8,526
|
|
2
|
+
grokbuildstreaming/stream.py,sha256=1OWsyJ-SVDT3z_XpuXl3LVkSbuCAl8cWUKX_pHXLEy8,13109
|
|
3
|
+
grokbuildstreaming-0.1.0.dist-info/METADATA,sha256=JZF2cx2CK-qBgssflSA8WLW-s-cP_MPn5A0_mDOeEdM,1963
|
|
4
|
+
grokbuildstreaming-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
grokbuildstreaming-0.1.0.dist-info/top_level.txt,sha256=V4yMQaeL8XYkd5X34FqCG49NYkMMV-aB2AcXDf5Qt58,19
|
|
6
|
+
grokbuildstreaming-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
grokbuildstreaming
|