from dialoghelper import *dialoghelper
from fastcore import tools
from fastcore.test import *Basics
a = 1
find_var('a')1
names_containing
def names_containing(
s:str
):Names containing s in IPython user namespace, or caller globals
names_containing('ipython')['get_ipython', 'ipython_shell', 'in_ipython', 'load_ipython_extension']
find_dname
def find_dname(
dname:NoneType=None, required:bool=True
):Return dname as a gateway-root-relative name without a leading slash. A leading / starts from the gateway root, and other names are relative to the current dialog’s folder.
find_dname()'aai-ws/dialoghelper/nbs/00_core.ipynb'
async def repro_executor():
return await asyncio.get_running_loop().run_in_executor(None, find_dname)
await repro_executor()'aai-ws/dialoghelper/nbs/00_core.ipynb'
find_dname('index.ipynb')'aai-ws/dialoghelper/nbs/index.ipynb'
find_dname('../index.ipynb')'aai-ws/dialoghelper/index.ipynb'
find_dname('/foo/bar.ipynb')'foo/bar.ipynb'
DialogAPIError
def DialogAPIError(
*args, **kwargs
):A Solveit API endpoint reported an error (its JSON response carried an error key)
call_endpa
async def call_endpa(
path, dname:str='', json:bool=False, raiseex:bool=False, id:NoneType=None, required:bool=True, timeout:int=10,
audit:bool=False, chkerr:bool=True, **data
):call_endp
def call_endp(
path, dname:str='', json:bool=False, raiseex:bool=False, id:NoneType=None, required:bool=True, timeout:int=10,
audit:bool=False, chkerr:bool=True, **data
):curr_dialog
async def curr_dialog(
dname:str='', # Dialog to get info for; defaults to current dialog
)->dict:Get the current dialog info.
await curr_dialog()add_html
def add_html(
content:str, # The HTML to send to the client (generally should include hx-swap-oob)
dname:str='', # Dialog to get info for; defaults to current dialog
):Send HTML to the browser to be swapped into the DOM
add_html_a
async def add_html_a(
content:str, # The HTML to send to the client (generally should include hx-swap-oob)
dname:str='', # Dialog to get info for; defaults to current dialog
):Send HTML to the browser to be swapped into the DOM
from fasthtml.common import *add_html(Div(P('Hi'), hx_swap_oob='beforeend:#dialog-container')){'success': 'Content added to DOM'}
add_scr
def add_scr(
scr, oob:str='innerHTML:#ephemeral'
):Swap a script element to the end of the ephemeral element
add_scr_a
async def add_scr_a(
scr, oob:str='innerHTML:#ephemeral'
):Swap a script element to the end of the ephemeral element
add_mod_a
async def add_mod_a(
s:str
):Wrap javascript code string in a js script module and add it via add_html
add_mod
def add_mod(
s:str
):Wrap javascript code string in a js script module and add it via add_html
iife
def iife(
code:str
):Wrap javascript code string in an IIFE and execute it via add_html
iife_a
async def iife_a(
code:str
):Wrap javascript code string in an IIFE and execute it via add_html
event_get
def event_get(
evt:str, timeout:int=15, **data
):Call fire_event and then pop_data to get a response
event_get_a
async def event_get_a(
evt:str, timeout:int=15, **data
):Call fire_event and then pop_data to get a response
trigger_now
def trigger_now(
evt, data:NoneType=None, ttl:int=5000
):Synchronously trigger a browser event, safe against replay
event_once_a
async def event_once_a(
evt, timeout:int=15, ttl:int=5000, **data
):Like event_get_a but with replay/dedup safety via trigger_now
event_once
def event_once(
evt, timeout:int=15, ttl:int=5000, **data
):Like event_get but with replay/dedup safety via trigger_now
iife("$('body').one('test_evt', e => pushData(e.detail.idx, {reply: 'it worked!'}));")
event_once('test_evt', data={'hello': 'world'}){'data_id': 'f14d1951-7e8f-47a9-9189-6299b8bd0c87', 'reply': 'it worked!'}js_run_a
async def js_run_a(
code
):Run JS code that calls done() when finished, and wait for result
r = js_run("setTimeout(() => done({answer: 6*7}), 100)")
r['answer']js_eval_a
async def js_eval_a(
expr
):Evaluate a JS expression in the browser and return the result
js_eval("return {width: window.innerWidth}"){'data_id': 'ee43ab10-480d-4469-9c5a-b3a63eb6f3f8', 'result': {'width': 1374}}js_eval("return document.querySelectorAll('.msg').length"){'data_id': 'ad5692cc-097e-498c-9adb-fca12fb12b94', 'result': 0}js_eval("a=1")
js_eval("return a+1"){'data_id': '3b056d62-bc76-4b23-a0be-394a336edc7b', 'result': 2}event_get, js_run, and js_eval each collect one response from the browser page. Use Channel for an ongoing exchange, such as an event stream or a protocol with repeated requests. It keeps a two-way WebSocket connection open instead of making an HTTP round trip for each message.
Channel
def Channel(
chan, url:NoneType=None
):Duplex JSON messaging with the other peers on a named /wsx relay channel
Connect each peer to the same channel name on Solveit’s /wsx relay. The relay forwards each frame unchanged to every other peer on that channel. solvecdp uses Channel to communicate with the solveit-chrome extension.
Channel sends JSON dictionaries. Pass the fields as keyword arguments to send. Incoming frames become AttrDict objects. If a frame’s id matches a pending request, it completes that request. Other frames go into the events queue.
Here we connect two peers and send a greeting. The second peer reads it from events.
ch1,ch2 = await Channel.connect('test'),await Channel.connect('test')
await ch1.send(hello='world')
test_eq((await ch2.events.get()).hello, 'world')request adds a unique id and waits for a reply with that id. Requests can run concurrently. The peer answering a request must include its id in the reply. This example replies with twice the supplied number.
async def _responder():
msg = await ch2.events.get()
await ch2.send(id=msg.id, result=msg.x*2)
task = asyncio.create_task(_responder())
test_eq((await ch1.request(x=21)).result, 42)Check is_open before and after closing the connections.
assert ch1.is_open
await ch1.close()
await ch2.close()
assert not ch1.is_opendisplay_response
def display_response(
display:str, result:str=None
):Return a special response where display is added as markdown/HTML to the prompt output, and result is returned to the LLM
realpath() returns the gateway’s on-disk root. realpath('') returns the current notebook’s folder. Other relative paths use that folder, regardless of the kernel’s cwd. A leading slash uses the gateway root. The binding follows notebook renames. Targets need not exist. .. can move up within the root. The gateway resolves symlinks within its root and rejects dangling symlinks and paths escaping the root.
These helpers use dh_settings['kid'], initialized from RUSTYGATE_KERNEL_ID. An unbound kernel can resolve root-qualified paths but has no notebook-relative base. list_dialogs() lists the current notebook’s folder by default and keeps the existing notebook-extension and folder-suffix conventions.
realpath
async def realpath(
subpath:str='/', # Gateway-root-relative with a leading slash, otherwise relative to the current notebook's folder
)->str:Get the on-disk path from the kernel’s current notebook binding.
await realpath('data/results')'/Users/jhoward'
list_dialogs
async def list_dialogs(
subpath:str='', # Path under data root (absolute with `/`, else relative to current dialog's folder)
depth:int=1, # Directory depth
)->dict:List dialogs and folders under subpath. Folders have / suffix.
(await list_dialogs())['items'][-4:]['06_utils.ipynb', '07_test.ipynb', 'data/', 'index.ipynb']
(await list_dialogs('/'))['items'][:5]['Applications/', 'CRAFT.ipynb', 'CRAFTs/', 'Desktop/', 'Documents/']
Dialog model
Dialog and Message extend aidialog’s classes with Solveit’s metadata fields. meta_attrs maps each attribute to its stored metadata key. Reading a message makes these fields available as attributes. Serializing the message writes them back to metadata.
At startup, a Solveit kernel registers this Dialog class with aidialog.dlgskill.set_dlg(path, cls=Dialog). Subsequent reads through aidialog.dlgskill use these subclasses for the current dialog.
Dialog
def Dialog(
messages:NoneType=None, name:str='', meta:NoneType=None
):Message
def Message(
content:str='', # The message text: markdown, code, or a prompt's request
output:str='', # Jupyter-style output list (code and prompt messages), or ''
id:NoneType=None, # Message id; 4 random hex bytes if None
msg_type:str='code', # One of `smsg_types`: code, note, prompt, or raw
attachments:NoneType=None, # `Attachment`s carried by the message
meta:NoneType=None, # Remaining cell metadata, carried verbatim through save/load
**xtras
):aidialog Message with solveit’s persisted fields declared
dlg_path
def dlg_path(
dname:str=''
):The .ipynb file for dname (default: the current dialog)
The editing tools use the gateway’s cells API. They send batches of operations to add or delete cells, merge metadata, or toggle flags. An edit that needs a message’s existing content reads that cell before sending its replacement.
Writes do not check whether the cell changed since the read. The last write to a field wins, as it did with direct file editing. Solveit updates its view from the gateway’s broadcasts rather than waiting for a file watcher to report a diff.
Gateway kernels have RUSTYGATE_URL in their environment. Set dh_settings['rusty'] to override it during development or tests. cells_client binds a client to one dialog’s notebook under the gateway’s data root. data_root resolves that root for operations that still use filesystem paths.
cells_client
def cells_client(
dname:str=''
):Create a cells client for the current kernel or a separately addressed notebook
View/edit dialog
s = 'x = 21\nx*2\ndone'
test_eq(_lnhashs_content(s), '\n'.join(lnhash(i,l)+l for i,l in enumerate(s.splitlines(),1)))
test_eq(_lnhashs_content(s, 2, -1).splitlines()[0], lnhash(2,'x*2')+'x*2')
test_eq(len(_lnhashs_content(s, 1, 2).splitlines()), 2)read_msg
async def read_msg(
n:int=-1, # Message index (if relative, +ve is downwards)
relative:bool=True, # Is `n` relative to current message (True) or absolute (False)?
id:str=None, # Message id to find (defaults to current message)
start_line:int=1, # Starting line to view
end_line:int=None, # End line (defaults to last line if None; -1 for EOF)
nums:bool=False, # Whether to show line numbers
lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers?
dname:str='', # Dialog to get info for; defaults to current dialog
)->dict:Get the message indexed in the current dialog. NB: Messages in the current dialog above the current message are already visible; use this only when you need line numbers for editing operations, or for messages not in the current dialog or below the current message. - To get the exact message use n=0 and relative=True together with id. - To get a relative message use n (relative position index). - To get the nth message use n with relative=False, e.g n=0 first message, n=-1 last message.
find_msgs
async def find_msgs(
re_pattern:str='', # Optional regex to search for (re.DOTALL+re.MULTILINE is used)
msg_type:str=None, # optional limit by message type ('code', 'note', or 'prompt')
before:int=0, # Include additional n msgs before matches
after:int=0, # Include additional n msgs after matches
context:int=None, # Include additional n msgs around matches (default 1, or 0 when `headers_only`)
use_case:bool=False, # Use case-sensitive matching?
use_regex:bool=True, # Use regex matching?
only_err:bool=None, # Only messages with errors if True, only those without if False, all if None
only_exp:bool=None, # Only exported messages if True, only unexported if False, all if None
ids:str='', # Optionally filter by comma-separated list of message ids
limit:int=None, # Optionally limit number of returned items
include_output:bool=True, # Include output in returned dict?
include_meta:bool=True, # Include all additional message metadata
as_xml:bool=False, # Use concise unescaped XML output format
nums:bool=False, # Show line numbers?
trunc_out:bool=False, # Middle-out truncate code output to 100 characters?
trunc_in:bool=False, # Middle-out truncate cell content to 80 characters?
headers_only:bool=False, # Only return note messages that are headers (first line only); cannot be used together with `header_section`
header_section:str=None, # Find section starting with this header; returns it plus all children
include_skipped:bool=False, # Include messages hidden from AI (skipped)?
dname:str='', # Dialog to get info for; defaults to current dialog
)->list[dict]: # Messages in requested dialog that contain the given informationOften it is more efficient to call view_dlg (not find_msgs) to see the whole dialog, so you can use it all from then on. Message ids are identical to those in LLM chat history, so do NOT call this to view a specific message if it’s in the chat history–instead use view_msg. Do NOT use find_msgs to view message content in the current dialog above the current prompt – these are already provided in LLM context, so just read the content there directly. (NB: LLM context only includes messages above the current prompt, whereas find_msgs can access all messages.) To refer to a found message from code, use its id field.
1+12
r = await find_msgs(r'1\+1', include_meta=False, include_output=True, context=0)
r[{'id': '8ce548d6',
'content': '1+1',
'output': '2',
'msg_type': 'code',
'exported': False},
{'id': '372c363d',
'content': "_id = await add_msg('1+1', run=True, msg_type='code')",
'output': '',
'msg_type': 'code',
'exported': False}]
hl_md(await find_msgs(r'1\+1', include_meta=False, as_xml=True, context=0))<msgs><code id="8ce548d6">1+1<out>2</out></code><code id="372c363d">_id = await add_msg('1+1', run=True, msg_type='code')</code></msgs>view_dlg
async def view_dlg(
dname:str='', # Dialog to get info for; defaults to current dialog
msg_type:str=None, # optional limit by message type ('code', 'note', or 'prompt')
nums:bool=False, # Whether to show line numbers
include_output:bool=False, # Include output in returned dict?
trunc_out:bool=True, # Middle-out truncate code output to 100 characters (only applies if `include_output`)?
trunc_in:bool=False, # Middle-out truncate cell content to 80 characters?
include_skipped:bool=False, # Include messages hidden from AI (skipped)?
)->str:Concise XML view of all messages (optionally filtered by type), not including metadata. Often it is more efficient to call this to see the whole dialog at once (including line numbers if needed), instead of running find_msgs or view_msg multiple times.
hl_md((await view_dlg(nums=True))[:500])<msgs><code id="955b9784"> 1 │ #| default_exp core</code><code id="a982e24d"> 1 │ from dialoghelper import *</code><markdown id="0aafe008"> 1 │ # dialoghelper
2 │
3 │ > Inspect and edit Solveit dialogs, run messages, manage kernels, and communicate with browser pages
4 │
5 │ Use these helpers from a Solveit kernel. Most examples below need a running Solveit server and a current dialog. They change that dialog and include examples that run prompts or restart and add_msg
async def add_msg(
content:str, # Content of the message (i.e the message prompt, code, or note text)
msg_type:str='note', # Message type, can be 'code', 'note', or 'prompt'
run:bool=False, # Run the message?
*,
placement:str='', # Location to place message. Can be 'at_start' or 'at_end', and if id provided or in curr dlg can also be 'add_after' or 'add_before'. Defaults to 'at_end' if no id and not targeting curr dlg
id:str=None, # id of message that placement is relative to (if None, uses current message)
dname:str='', # Dialog to add to; defaults to current dialog (`run` only has an effect if dialog is currently running)
output:str='', # Prompt/code output; Code outputs must be .ipynb-compatible JSON array
exported:int | None=0, # Mark message as exported (stored as nbdev `export` metadata)?
skipped:int | None=0, # Hide message from prompt?
i_collapsed:int | None=0, # Collapse input?
o_collapsed:int | None=0, # Collapse output?
heading_collapsed:int | None=0, # Collapse heading section?
pinned:int | None=0, # Pin to context?
)->str: # Message ID of newly created messageAdd/update a message to the queue to show after code execution completes, and optionally run it. Code messages are run using python’s restricted sandbox. NB: when creating multiple messages in a row, after the 1st message set id to the result of the last add_msg call, otherwise messages will appear in the dialog in REVERSE order.
_id = await add_msg('testing')
_id'ee150d1b'
'321f8ade'
read_msgid
async def read_msgid(
id:str, # Message id to find
start_line:int=1, # Starting line to view
end_line:int=None, # End line (defaults to last line if None; -1 for EOF)
nums:bool=False, # Whether to show line numbers
lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers?
dname:str='', # Dialog to get message from; defaults to current dialog
add_to_dlg:bool=False, # Whether to add message content to current dialog (as a raw message)
)->dict:Get message id. Message IDs can be view directly in LLM chat history/context, or found in find_msgs results. Use add_to_dlg if the LLM or human may need to refer to the message content again later.
r = await read_msg(-2)
print((await read_msg(-2)).content)--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[133], line 2 1 r = await read_msg(-2) ----> 2 print((await read_msg(-2)).content) AttributeError: 'dict' object has no attribute 'content'
read_msg returns a dict2obj result. Access its fields as dictionary keys, such as r['content'], or attributes, such as r.content. Endpoint helpers also provide attribute access for JSON object responses.
bmsg = await add_msg('at bottom', placement='at_end')test_eq((await read_msg(-1, relative=False))['id'], bmsg) # at_end put it lastview_msg
async def view_msg(
id:str, # Message id to view
dname:str='', # Dialog to get message from; defaults to current dialog
nums:bool=True, # Whether to show line numbers
lnhashs:bool=False, # Show exhash `lineno|hash|` addresses instead of line numbers?
start_line:int=1, # Starting line to view. Rarely needed--read whole message in nearly all cases instead
end_line:int=None, # End line (defaults to last line if None; -1 for EOF)
incl_out:bool=False, # Append the message's output in an `<out>` block?
trunc_out:bool=True, # Truncate an included output to ~512 chars?
add_to_dlg:bool=False, # Whether to add message content to current dialog (as a raw message)
)->str:Views the content* of message id. Same as read_msgid(...)['content'], defaulting to nums=True.* Use add_to_dlg if the LLM or human may need to refer to the message content again later.
print((await view_msg(r.id))) 1 │ testing
# dh_settings['dname'] = 'tmp'
# _id = await add_msg('testing', placement='at_end')
# print(_id)
# del(dh_settings['dname'])msg_ref
def msg_ref(
id, dname:NoneType=None
):Markdown ref to a message — same-dialog #_id or cross-dialog #dname/_id (anchors target DOM ids, which carry a _ prefix)
del_msgs
async def del_msgs(
ids:str=None, # Comma-separated ids of message(s) to delete
dname:str='', # Dialog to get info for; defaults to current dialog
log_changed:bool=False, # Add a note showing the deleted content?
)->list:Delete exactly the named messages (a collapsed heading’s hidden section stays). DO NOT USE THIS unless you have been explicitly instructed to delete messages.
await del_msgs(bmsg)
await del_msgs(_id)['ee150d1b']
['4bd0fdaf']
_id = await add_msg('1+1', run=True, msg_type='code')await del_msgs(_id)['b001fd04']
_id = await add_msg('Hi', run=True, msg_type='prompt')await del_msgs(_id)['4561db1c']
run_and_prompt
async def run_and_prompt(
code:str, # Python code to run
prompt:str='Continue.', # Prompt to add after code execution
)->str:Run code and then run prompt, returning the resulting message ID.
update_msg
async def update_msg(
id:str=None, # id of message to update (if None, uses current message)
msg:Optional[Dict]=None, # Dictionary of field keys/values to update
dname:str='', # Dialog to get info for; defaults to current dialog
log_changed:bool=False, # Add a note showing the diff?
*, content:str | None=None, # Content of the message (i.e the message prompt, code, or note text)
msg_type:str | None=None, # Message type, can be 'code', 'note', or 'prompt'
output:str | None=None, # Prompt/code output; Code outputs must be .ipynb-compatible JSON array
exported:int | None=None, # Set export state (nbdev `export` metadata)?
skipped:int | None=None, # Hide message from prompt?
i_collapsed:int | None=None, # Collapse input?
o_collapsed:int | None=None, # Collapse output?
heading_collapsed:int | None=None, # Collapse heading section?
pinned:int | None=None, # Pin to context?
meta:dict | None=None, # Replace message meta wholesale
mergemeta:dict | None=None, # Deep-merge into message meta; a `None` value deletes its key
)->str:Update an existing message. Provide either msg OR field key/values to update. - Use content param to update contents. - Only include parameters to update–missing ones will be left unchanged.
_id = await add_msg('testing')_id = await update_msg(_id, content='toasting')_id = await update_msg(_id, skipped=1)update_msg follows Message.update for metadata. meta= replaces the metadata dictionary. mergemeta= merges nested dictionaries and deletes keys whose value is None.
A metadata merge on its own uses a gateway merge operation. It follows JSON merge-patch rules, where null deletes a key. A metadata replacement, or a merge combined with content changes, uses the serialized message in an unconditional update operation.
await update_msg(_id, mergemeta=dict(pins=dict(a=1)))
test_eq((await read_msgid(_id))['meta']['pins'], {'a': 1})
await update_msg(_id, mergemeta=dict(pins=None))
assert 'pins' not in (await read_msgid(_id))['meta']msg = await read_msgid(_id)
msg['content'] = 'toasted'
await update_msg(msg=msg)'d8addd7b'
await del_msgs(_id)--------------------------------------------------------------------------- CancelledError Traceback (most recent call last) Cell In[177], line 1 ----> 1 await del_msgs(_id) Cell In[149], line 14, in del_msgs(ids, dname, log_changed) 10 logs = [] 11 if log_changed: 12 by = {c['id']: c for c in await fc.cells(ids=ids)} 13 logs = [f"> Deleted {msg_ref(i, dname)}\n\n```\n{_cell2dict(by[i])['content']}\n```" for i in ids if i in by] ---> 14 await fc.apply([dict(op='delete', id=i) for i in ids]) 15 for l in logs: await add_msg(l) 16 return ids File ~/aai-ws/jupyasyncclient/jupyasyncclient/files.py:166, in apply(self, ops) 163 @patch 164 async def apply(self:JupyAsyncCellsClient, ops): 165 "Apply `ops` atomically, returning ids of added cells." --> 166 m = await self._op(self.api.cells.post_cells, path=self.path, ops=ops) 167 self.hash = m['hash'] 168 return m['added_ids'] File ~/aai-ws/jupyasyncclient/jupyasyncclient/files.py:37, in _op(self, op, **kw) 35 "Call spec op `op` with `session_id` attached and None values dropped; a conditional-write 409 raises `HashMismatch`." 36 kw = {k:v for k,v in dict(kw, session_id=self.session_id).items() if v is not None} ---> 37 try: return await op(**kw) 38 except APIError as e: 39 if e.status_code==409 and isinstance(e.raw, dict) and 'hash' in e.raw: raise HashMismatch(e.raw['hash']) from e File ~/aai-ws/fastspec/fastspec/oapi.py:148, in __call__(self, *args, **kwargs) 146 stream, url, headers, query, route, kw = self._prep(args, kwargs) 147 if stream: return self._stream(url, headers=headers, query=query, route=route, **kw) --> 148 return await self._request(url, headers=headers, query=query, route=route, **kw) File ~/aai-ws/fastspec/fastspec/oapi.py:120, in _request(self, url, headers, query, body, route, **kwargs) 116 @patch 117 @delegates(AsyncTransport.request) # files, raw 118 async def _request(self:OpFunc, url, *, headers=None, query=None, body=None, route=None, **kwargs): 119 "Execute an HTTP request and return decoded response." --> 120 try: return dict2obj(await self.client.request(self.verb, url, headers=headers, params=query, json=body, **kwargs)) 121 except Exception as e: self._raise_with_context(e, endpoint='', route=route, query=query, body=body) File ~/aai-ws/fasttransport/fasttransport/core.py:58, in AsyncTransport.request(self, method, url, headers, params, json, data, files, content, raw) 56 "Execute a request and decode JSON/text/binary response." 57 async with self._client() as client: ---> 58 resp = await client.request(method, url, headers=self._request_headers(headers, files=files), 59 params=params, json=json, data=data, files=files, content=content) 60 try: resp.raise_for_status() 61 except httpx2.HTTPStatusError as e: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1635, in AsyncClient.request(self, method, url, content, data, files, json, params, headers, cookies, auth, follow_redirects, timeout, extensions) 1620 warnings.warn(message, DeprecationWarning, stacklevel=2) 1622 request = self.build_request( 1623 method=method, 1624 url=url, (...) 1633 extensions=extensions, 1634 ) -> 1635 return await self.send(request, auth=auth, follow_redirects=follow_redirects) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1818, in AsyncClient.send(self, request, stream, auth, follow_redirects) 1814 self._set_timeout(request) 1816 auth = self._build_request_auth(request, auth) -> 1818 response = await self._send_handling_auth( 1819 request, 1820 auth=auth, 1821 follow_redirects=follow_redirects, 1822 history=[], 1823 ) 1824 try: 1825 if not stream: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1846, in AsyncClient._send_handling_auth(self, request, auth, follow_redirects, history) 1843 request = await auth_flow.__anext__() 1845 while True: -> 1846 response = await self._send_handling_redirects( 1847 request, 1848 follow_redirects=follow_redirects, 1849 history=history, 1850 ) 1851 try: 1852 try: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1881, in AsyncClient._send_handling_redirects(self, request, follow_redirects, history) 1878 for hook in self._event_hooks["request"]: 1879 await hook(request) -> 1881 response = await self._send_single_request(request) 1882 try: 1883 for hook in self._event_hooks["response"]: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1915, in AsyncClient._send_single_request(self, request) 1912 raise RuntimeError("Attempted to send a sync request with an AsyncClient instance.") 1914 with request_context(request=request): -> 1915 response = await transport.handle_async_request(request) 1917 assert isinstance(response.stream, AsyncByteStream) 1918 response.request = request File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_transports/default.py:388, in AsyncHTTPTransport.handle_async_request(self, request) 375 req = httpcore2.Request( 376 method=request.method, 377 url=httpcore2.URL( (...) 385 extensions=request.extensions, 386 ) 387 with map_httpcore_exceptions(): --> 388 resp = await self._pool.handle_async_request(req) 390 assert isinstance(resp.stream, typing.AsyncIterable) 392 return Response( 393 status_code=resp.status, 394 headers=resp.headers, 395 stream=AsyncResponseStream(resp.stream), 396 extensions=resp.extensions, 397 ) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/connection_pool.py:242, in AsyncConnectionPool.handle_async_request(self, request) 239 closing = self._assign_requests_to_connections() 241 await self._close_connections(closing) --> 242 raise exc from None 244 # Return the response. Note that in this case we still have to manage 245 # the point at which the response is closed. 246 assert isinstance(response.stream, typing.AsyncIterable) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/connection_pool.py:224, in AsyncConnectionPool.handle_async_request(self, request) 220 connection = await pool_request.wait_for_connection(timeout=timeout) 222 try: 223 # Send the request on the assigned connection. --> 224 response = await connection.handle_async_request(pool_request.request) 225 except ConnectionNotAvailable: 226 # In some cases a connection may initially be available to 227 # handle a request, but then become unavailable. 228 # 229 # In this case we clear the connection and try again. 230 pool_request.clear_connection() File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/connection.py:96, in AsyncHTTPConnection.handle_async_request(self, request) 93 self._connect_failed = True 94 raise exc ---> 96 return await self._connection.handle_async_request(request) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:125, in AsyncHTTP11Connection.handle_async_request(self, request) 123 async with Trace("response_closed", logger, request) as trace: 124 await self._response_closed() --> 125 raise exc File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:97, in AsyncHTTP11Connection.handle_async_request(self, request) 88 pass 90 async with Trace("receive_response_headers", logger, request, kwargs) as trace: 91 ( 92 http_version, 93 status, 94 reason_phrase, 95 headers, 96 trailing_data, ---> 97 ) = await self._receive_response_headers(**kwargs) 98 trace.return_value = ( 99 http_version, 100 status, 101 reason_phrase, 102 headers, 103 ) 105 network_stream = self._network_stream File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:167, in AsyncHTTP11Connection._receive_response_headers(self, request) 164 timeout = timeouts.get("read", None) 166 while True: --> 167 event = await self._receive_event(timeout=timeout) 168 if isinstance(event, h11.Response): 169 break File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:200, in AsyncHTTP11Connection._receive_event(self, timeout) 197 event = self._h11_state.next_event() 199 if event is h11.NEED_DATA: --> 200 data = await self._network_stream.read(self.READ_NUM_BYTES, timeout=timeout) 202 # If we feed this case through h11 we'll raise an exception like: 203 # 204 # httpcore2.RemoteProtocolError: can't handle event type (...) 208 # perspective. Instead we handle this case distinctly and treat 209 # it as a ConnectError. 210 if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_backends/anyio.py:37, in AnyIOStream.read(self, max_bytes, timeout) 35 with anyio.fail_after(timeout): 36 try: ---> 37 return await self._stream.receive(max_bytes=max_bytes) 38 except anyio.EndOfStream: # pragma: no cover 39 return b"" File ~/aai-ws/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py:1337, in SocketStream.receive(self, max_bytes) 1331 if ( 1332 not self._protocol.read_event.is_set() 1333 and not self._transport.is_closing() 1334 and not self._protocol.is_at_eof 1335 ): 1336 self._transport.resume_reading() -> 1337 await self._protocol.read_event.wait() 1338 self._transport.pause_reading() 1339 else: File ~/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/asyncio/locks.py:213, in Event.wait(self) 211 self._waiters.append(fut) 212 try: --> 213 await fut 214 return True 215 finally: CancelledError:
_edit_id = await add_msg('This message should be found.\n\nThis is a multiline message.')
_edit_id'ca8a365a'
This message should be found.
This is a multiline message.
This message should be found.
This is a multiline message.
This message should be found.
This is a multiline message.
This message should be found.
This is a multiline message.
print((await read_msg())['content'])--------------------------------------------------------------------------- CancelledError Traceback (most recent call last) Cell In[181], line 1 ----> 1 print((await read_msg())['content']) Cell In[107], line 24, in read_msg(n, relative, id, start_line, end_line, nums, lnhashs, dname) 20 fc = cells_client(dname) 21 if relative: 22 if not id: id = aidialog.dlgskill.cur_msgid 23 if not id: return {'msg':None} ---> 24 cells = await fc.cells(ids=id, context=abs(n) or None) 25 anchor = first(i for i,c in enumerate(cells) if c['id']==id) 26 if anchor is None: return {'msg':None} 27 if not 0<=anchor+n<len(cells): return {'msg':None} File ~/aai-ws/jupyasyncclient/jupyasyncclient/files.py:147, in cells(self, ids, idx, q, cell_type, meta, meta_not, limit, context, fields) 134 @patch 135 async def cells(self:JupyAsyncCellsClient, 136 ids=None, # Cell ids to keep: comma-separated str, or a list (...) 144 fields=None, # Comma-separated extras: 'hashes', 'meta', 'attachments' 145 ): 146 "The notebook's cells in document order, optionally selected and filtered (the gateway's cells GET stages)." --> 147 m = await self._op(self.api.cells.get_cells, path=self.path, ids=_cs(ids), idx=_cs(idx), q=q, cell_type=cell_type, 148 meta=None if meta is None else json.dumps(meta), meta_not=None if meta_not is None else json.dumps(meta_not), 149 limit=limit, context=context, fields=fields) 150 self.hash = m['hash'] 151 self.matched = m.get('matched') File ~/aai-ws/jupyasyncclient/jupyasyncclient/files.py:37, in _op(self, op, **kw) 35 "Call spec op `op` with `session_id` attached and None values dropped; a conditional-write 409 raises `HashMismatch`." 36 kw = {k:v for k,v in dict(kw, session_id=self.session_id).items() if v is not None} ---> 37 try: return await op(**kw) 38 except APIError as e: 39 if e.status_code==409 and isinstance(e.raw, dict) and 'hash' in e.raw: raise HashMismatch(e.raw['hash']) from e File ~/aai-ws/fastspec/fastspec/oapi.py:148, in __call__(self, *args, **kwargs) 146 stream, url, headers, query, route, kw = self._prep(args, kwargs) 147 if stream: return self._stream(url, headers=headers, query=query, route=route, **kw) --> 148 return await self._request(url, headers=headers, query=query, route=route, **kw) File ~/aai-ws/fastspec/fastspec/oapi.py:120, in _request(self, url, headers, query, body, route, **kwargs) 116 @patch 117 @delegates(AsyncTransport.request) # files, raw 118 async def _request(self:OpFunc, url, *, headers=None, query=None, body=None, route=None, **kwargs): 119 "Execute an HTTP request and return decoded response." --> 120 try: return dict2obj(await self.client.request(self.verb, url, headers=headers, params=query, json=body, **kwargs)) 121 except Exception as e: self._raise_with_context(e, endpoint='', route=route, query=query, body=body) File ~/aai-ws/fasttransport/fasttransport/core.py:58, in AsyncTransport.request(self, method, url, headers, params, json, data, files, content, raw) 56 "Execute a request and decode JSON/text/binary response." 57 async with self._client() as client: ---> 58 resp = await client.request(method, url, headers=self._request_headers(headers, files=files), 59 params=params, json=json, data=data, files=files, content=content) 60 try: resp.raise_for_status() 61 except httpx2.HTTPStatusError as e: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1635, in AsyncClient.request(self, method, url, content, data, files, json, params, headers, cookies, auth, follow_redirects, timeout, extensions) 1620 warnings.warn(message, DeprecationWarning, stacklevel=2) 1622 request = self.build_request( 1623 method=method, 1624 url=url, (...) 1633 extensions=extensions, 1634 ) -> 1635 return await self.send(request, auth=auth, follow_redirects=follow_redirects) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1818, in AsyncClient.send(self, request, stream, auth, follow_redirects) 1814 self._set_timeout(request) 1816 auth = self._build_request_auth(request, auth) -> 1818 response = await self._send_handling_auth( 1819 request, 1820 auth=auth, 1821 follow_redirects=follow_redirects, 1822 history=[], 1823 ) 1824 try: 1825 if not stream: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1846, in AsyncClient._send_handling_auth(self, request, auth, follow_redirects, history) 1843 request = await auth_flow.__anext__() 1845 while True: -> 1846 response = await self._send_handling_redirects( 1847 request, 1848 follow_redirects=follow_redirects, 1849 history=history, 1850 ) 1851 try: 1852 try: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1881, in AsyncClient._send_handling_redirects(self, request, follow_redirects, history) 1878 for hook in self._event_hooks["request"]: 1879 await hook(request) -> 1881 response = await self._send_single_request(request) 1882 try: 1883 for hook in self._event_hooks["response"]: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_client.py:1915, in AsyncClient._send_single_request(self, request) 1912 raise RuntimeError("Attempted to send a sync request with an AsyncClient instance.") 1914 with request_context(request=request): -> 1915 response = await transport.handle_async_request(request) 1917 assert isinstance(response.stream, AsyncByteStream) 1918 response.request = request File ~/aai-ws/.venv/lib/python3.13/site-packages/httpx2/_transports/default.py:388, in AsyncHTTPTransport.handle_async_request(self, request) 375 req = httpcore2.Request( 376 method=request.method, 377 url=httpcore2.URL( (...) 385 extensions=request.extensions, 386 ) 387 with map_httpcore_exceptions(): --> 388 resp = await self._pool.handle_async_request(req) 390 assert isinstance(resp.stream, typing.AsyncIterable) 392 return Response( 393 status_code=resp.status, 394 headers=resp.headers, 395 stream=AsyncResponseStream(resp.stream), 396 extensions=resp.extensions, 397 ) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/connection_pool.py:242, in AsyncConnectionPool.handle_async_request(self, request) 239 closing = self._assign_requests_to_connections() 241 await self._close_connections(closing) --> 242 raise exc from None 244 # Return the response. Note that in this case we still have to manage 245 # the point at which the response is closed. 246 assert isinstance(response.stream, typing.AsyncIterable) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/connection_pool.py:224, in AsyncConnectionPool.handle_async_request(self, request) 220 connection = await pool_request.wait_for_connection(timeout=timeout) 222 try: 223 # Send the request on the assigned connection. --> 224 response = await connection.handle_async_request(pool_request.request) 225 except ConnectionNotAvailable: 226 # In some cases a connection may initially be available to 227 # handle a request, but then become unavailable. 228 # 229 # In this case we clear the connection and try again. 230 pool_request.clear_connection() File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/connection.py:96, in AsyncHTTPConnection.handle_async_request(self, request) 93 self._connect_failed = True 94 raise exc ---> 96 return await self._connection.handle_async_request(request) File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:125, in AsyncHTTP11Connection.handle_async_request(self, request) 123 async with Trace("response_closed", logger, request) as trace: 124 await self._response_closed() --> 125 raise exc File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:97, in AsyncHTTP11Connection.handle_async_request(self, request) 88 pass 90 async with Trace("receive_response_headers", logger, request, kwargs) as trace: 91 ( 92 http_version, 93 status, 94 reason_phrase, 95 headers, 96 trailing_data, ---> 97 ) = await self._receive_response_headers(**kwargs) 98 trace.return_value = ( 99 http_version, 100 status, 101 reason_phrase, 102 headers, 103 ) 105 network_stream = self._network_stream File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:167, in AsyncHTTP11Connection._receive_response_headers(self, request) 164 timeout = timeouts.get("read", None) 166 while True: --> 167 event = await self._receive_event(timeout=timeout) 168 if isinstance(event, h11.Response): 169 break File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_async/http11.py:200, in AsyncHTTP11Connection._receive_event(self, timeout) 197 event = self._h11_state.next_event() 199 if event is h11.NEED_DATA: --> 200 data = await self._network_stream.read(self.READ_NUM_BYTES, timeout=timeout) 202 # If we feed this case through h11 we'll raise an exception like: 203 # 204 # httpcore2.RemoteProtocolError: can't handle event type (...) 208 # perspective. Instead we handle this case distinctly and treat 209 # it as a ConnectError. 210 if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE: File ~/aai-ws/.venv/lib/python3.13/site-packages/httpcore2/_backends/anyio.py:37, in AnyIOStream.read(self, max_bytes, timeout) 35 with anyio.fail_after(timeout): 36 try: ---> 37 return await self._stream.receive(max_bytes=max_bytes) 38 except anyio.EndOfStream: # pragma: no cover 39 return b"" File ~/aai-ws/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py:1337, in SocketStream.receive(self, max_bytes) 1331 if ( 1332 not self._protocol.read_event.is_set() 1333 and not self._transport.is_closing() 1334 and not self._protocol.is_at_eof 1335 ): 1336 self._transport.resume_reading() -> 1337 await self._protocol.read_event.wait() 1338 self._transport.pause_reading() 1339 else: File ~/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/asyncio/locks.py:213, in Event.wait(self) 211 self._waiters.append(fut) 212 try: --> 213 await fut 214 return True 215 finally: CancelledError:
print((await read_msg(n=0, id=_edit_id, nums=True))['content'])print((await read_msg(n=0, id=_edit_id, nums=True, start_line=2, end_line=3))['content'])run_msg
async def run_msg(
ids:str=None, # Comma-separated ids of message(s) to execute
dname:str='', # Running dialog to get info for; defaults to current dialog. (Note dialog *must* be running for this function)
):Adds a message to the run queue. Use read_msg to see the output once it runs.
codeid = (await read_msg())['id']await run_msg(codeid)copy_msgs
async def copy_msgs(
ids:str=None, # Comma-separated ids of message(s) to copy
cut:bool=False, # Cut message(s)? (If not, copies)
dname:str='', # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)
)->dict:Add ids to clipboard.
paste_msgs
async def paste_msgs(
id:str=None, # Message id to paste next to
after:bool=True, # Paste after id? (If not, pastes before)
dname:str='', # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)
)->dict:Paste clipboard msg(s) after/before the current selected msg (id).
await copy_msgs(codeid)with expect_fail(DialogAPIError, contains='Dialog /dlg/nonexistent.ipynb may not be running'):
await copy_msgs('_fake', dname='/dlg/nonexistent.ipynb')tgt = (await read_msg())['id']await paste_msgs(tgt)# TODO fix so this passes
# with expect_fail(DialogAPIError, contains='Dialog /dlg/nonexistent may not be running'):
# await paste_msgs('_fake', dname='/dlg/nonexistent')newmsg = await read_msg(1, id=tgt)
newmsg['content']await del_msgs(newmsg['id'])enable_mermaid()mermaid('graph LR; A[Start] --> B[Process]; B --> C[End];')You can also add to a note:
```mermaid
graph LR
A[Start] --> B[Process]
B --> C[End]
```
This renders as:
graph LR
A[Start] --> B[Process]
B --> C[End]
toggle_header
async def toggle_header(
id:str, # id of markdown header note message to toggle collapsed state
dname:str='', # Dialog to toggle in; defaults to current dialog
)->dict:Toggle collapsed header state for id
toggle_bookmark
async def toggle_bookmark(
id:str, # id of message to toggle bookmark on
n:int, # Bookmark number (1-9)
dname:str='', # Dialog to set bookmark in; defaults to current dialog
)->dict:Toggle numbered bookmark (1-9) on a message, clearing it from any other message when setting
toggle_export
async def toggle_export(
ids:str | list, # Message id(s) to toggle (comma-separated str, or list)
dname:str='', # Dialog to get info for; defaults to current dialog
)->dict:Toggle the nbdev export directive (meta form) on each message, independently
toggle_comment
async def toggle_comment(
id:str, # id of code message (or comma-separated ids) to toggle comments on
dname:str='', # Dialog to toggle comments in; defaults to current dialog
)->dict:Toggle line comments on code message(s). If any lines are uncommented, comments all; otherwise uncomments all.
await toggle_comment(codeid) # comment
await toggle_comment(codeid) # uncommentwith expect_fail(contains='such file'):
await toggle_comment('_fake', dname='/dlg/nonexistent.ipynb')test header
header end
hdid = (await read_msg())['id']await toggle_header(hdid)with expect_fail(APIError, contains='no such file'):
await toggle_header('_fake', dname='/dlg/nonexistent.ipynb')Dlg conveniences
url2note
async def url2note(
url:str, # URL to read
extract_section:bool=True, # If url has an anchor, return only that section
selector:str=None, # Select section(s) using BeautifulSoup.select (overrides extract_section)
ai_img:bool=True, # Make images visible to the AI
split_re:str='', # Regex to split content into multiple notes, set to '' for single note
):Read URL as markdown, and add note(s) below current message with the result
_id = await url2note('https://docs.python.org')await del_msgs(_id)create_or_run_dialog
async def create_or_run_dialog(
name:str, # Name/path of the dialog (relative to current dialog's folder, or absolute if starts with '/')
template:bool=True, # Include TEMPLATE.ipynb files when creating a new dialog
):Create a new dialog, or set an existing one running
await create_or_run_dialog('test_dialog.ipynb')restart_dialog
async def restart_dialog(
name:str, # Name/path of the dialog (relative to current dialog's folder, or absolute if starts with '/')
):Restart a dialog’s kernel, starting the dialog first if needed
await restart_dialog('test_dialog.ipynb')stop_dialog
async def stop_dialog(
name:str, # Name/path of the dialog (relative to current dialog's folder, or absolute if starts with '/')
):Stop a running dialog kernel
await stop_dialog('test_dialog.ipynb')rm_dialog
async def rm_dialog(
name:str, # Name/path of the dialog to delete (relative to current dialog's folder, or absolute if starts with '/')
):Delete a dialog (or folder) and associated records, stopping the kernel if running
await rm_dialog('test_dialog.ipynb')run_code_interactive
async def run_code_interactive(
code:str, # Code to have user run
):Insert code into user’s dialog and request for the user to run it. Use other functions where possible, but if they can not find needed information, ALWAYS use this instead of guessing or giving up. IMPORTANT: This tool is TERMINAL - after calling it, you MUST stop all tool usage and wait for user response. Never call additional tools after this one.
Message.execute and Dialog.execute run messages in the current Solveit kernel. They replace aidialog’s default CaptureShell execution, which uses a temporary shell. The Solveit server handles these runs like user-initiated runs, including attribution, the spinner, streamed card updates, and saving to disk. Awaiting execute returns the messages’ outputs.
A tool can call execute while its own message is still running. Adding that work to the main shell’s serial queue would deadlock. execute routes the nested work through the kernel’s persistent sidecar subshell instead. The sidecar runs it on a separate serial queue while the calling message waits.
When py runs with safepyrun’s audit checks, the nested execution remains inside that audit window. execute does not add a separate sandbox.
Dialog.execute
async def execute(
*ids, above:bool=False, # Include each matched cell and all cells above it?
below:bool=False, # Include each matched cell and all cells below it?
all:bool=False, # Include all code cells (ignores `msgids`)?
exported:bool=False, # Only cells with `#| export` or `#| exports`?
default_eval:bool=True, # Participation default when neither the cell nor the notebook has an `eval` directive
ignore_eval:bool=False, # Skip `eval` filtering entirely: every selected cell runs
):Run the selected code messages through Solveit and return their outputs
d = Dialog(name='mm')
m = d.mk_message('x', msg_type='note', meta=dict(hide_input=True, bookmark=3))
test_eq((m.i_collapsed, m.bookmark), (True, 3)) # declared fields promote at construction
m.i_collapsed = 1
test_eq(m.cell_meta(), dict(hide_input=True, bookmark=3)) # ints demote as booleans, keys per the file conventionText Edit
await msg_insert_line(_edit_id, 0, 'This should go to the first line')
await msg_insert_line(_edit_id, 3, 'This should go to the 4th line')
print(await msg_insert_line(_edit_id, 5, 'This should go to the last line'))print((await read_msg(n=0, id=_edit_id, nums=True))['content'])print(await msg_str_replace(_edit_id, 'This should go to the first line', 'This should go to the 1st line'))for f in (msg_str_replace, msg_strs_replace):
assert 'use_regex' in inspect.signature(f).parametersprint((await read_msg(n=0, id=_edit_id, nums=True))['content'])print(await msg_strs_replace(_edit_id, ['This is a multiline message.', 'This should go to the last line'], ['5th line', 'last line']))print((await read_msg(n=0, id=_edit_id, nums=True))['content'])print(await msg_replace_lines(_edit_id, 2, 4,'line 2\nline 3\nline 4\n'))print((await read_msg(n=0, id=_edit_id, nums=True))['content'])print(await msg_del_lines(_edit_id, 2, 4))print((await read_msg(n=0, id=_edit_id, nums=True)).content)await del_msgs(_edit_id)id1 = await add_msg('hello world')
id2 = await add_msg('hello there', id=id1)results = await msg_str_replace([id1, id2], 'hello', 'hi')
print(results)await del_msgs(id1)
await del_msgs(id2)Help
solveit_docs
async def solveit_docs():Full reference documentation for Solveit - use this to answer questions about how to use Solveit. NB: The whole docs fit in LLM context, so read the whole thing, don’t search/filter it. Always re-run rather than relying on truncated history or assumptions.
dialog_link
def dialog_link(
dname:str='', # Dialog to link to (relative to current dialog's folder, or absolute if starts with '/'); empty for in-page anchor link
msg_id:str=None, # Optional message id to scroll to
):Return an IPython HTML link to open a dialog in Solveit. After calling this, output the resulting HTML anchor tag exactly as returned—do not wrap in a fenced code block or convert to markdown link format.
dialog_link(msg_id='a7d82acd')dialog_link(dname='/CRAFT.ipynb')dialog_link(dname='/CRAFT.ipynb', msg_id='ce727fd8')spawn_agent
async def spawn_agent(
prompt:str
):Spawn a subagent to complete a task defined by prompt. Must be run as a tool - not from Python. The subagent’s context and tools is defined by the parent prompt’s history
testing