Changelog Archive - June 2026
June 2026
MCP server now exposes Resources and Prompts, and pushes listChanged notifications.
Scriptling’s MCP server implements the remaining two MCP primitives alongside tools, all defined as files in folders — no Go code, no startup registration:
- Tools (
--mcp-tools):name.toml+name.py, as before. - Resources (
--mcp-resources): just files — the path is the URI (the first directory is the scheme, the rest mirrors the URI). A{var}segment with a.pyis a template (the script runs with the extracted variable); every other file is served verbatim (a.pywith no{var}is served as source text). A template or static resource may have an optional_stem.tomlmetadata file providing a human-readable name, description, and MIME type (_prefix files are never served). - Prompts (
--mcp-prompts):name.mdfor a static prompt, orname.toml+name.pyfor an arg-driven prompt (args declared in the toml, the script returns messages).
A built-in scriptling://script/{name} resource template (tool source code)
and a write_script prompt are always available. The scriptling.mcp.Client class
gained list_resources, list_resource_templates, read_resource,
list_prompts, and get_prompt for reading them on remote servers.
# A templated resource from a file: resources/greeting/{name}.py -> greeting://{name}
scriptling --mcp-resources ./resources
curl -X POST http://127.0.0.1:8000/mcp -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"resources/read","params":{"uri":"greeting://Ada"}}'scriptling.mcp.Client can read resources and prompts from MCP servers.
The MCP client class gained five methods for the resource and prompt primitives:
| Method | Description |
|---|---|
client.list_resources() |
List static resources (uri, name, description, mimeType) |
client.list_resource_templates() |
List resource templates (uriTemplate, …) |
client.read_resource(uri) |
Read a resource (static or expanded from a template) |
client.list_prompts() |
List prompts (name, description, arguments) |
client.get_prompt(name, arguments) |
Render a prompt into messages |
import scriptling.mcp as mcp
c = mcp.Client("https://example.com/mcp")
data = c.read_resource("config://app") # -> {uri, mimeType, text|blob}
out = c.get_prompt("write_script", {"task": "greet a user"})
for msg in out.messages:
print(msg.role, msg.content)MCP over stdio: Scriptling can run as a stdio MCP server, and consume stdio MCP servers.
Scriptling can now serve the Model Context Protocol over stdin/stdout
(newline-delimited JSON-RPC 2.0) — the transport MCP hosts use to launch a
server as a subprocess — in addition to HTTP. The MCP tool flags enable the
server; leaving off --server serves it over stdio:
scriptling --mcp-tools ./tools # serve your tools over stdio
scriptling --mcp-exec-script # serve the code-execution toolThis mirrors --json-rpc: stdio by default, HTTP with --server. As with
--json-rpc, logs are redirected to stderr so stdout stays a clean protocol
stream. With --server, the same tools continue to be served over HTTP at
/mcp.
scriptling.mcp.Client() now chooses HTTP or stdio from its argument.
The client transport is selected from the first argument: an http:// or
https:// URL connects over HTTP; anything else is launched as a local stdio
MCP server subprocess. A new args keyword supplies the server’s command-line
arguments, and a new client.close() method shuts a stdio subprocess down.
import scriptling.mcp as mcp
# HTTP server
remote = mcp.Client("https://example.com/mcp", namespace="t2", bearer_token="secret")
# stdio server (a local executable)
local = mcp.Client("/usr/local/bin/thebinary", args=["--server"], namespace="t1")
print(local.call_tool("t1__greet", {"name": "Ada"}))
local.close()bearer_token is HTTP-only and args is stdio-only; using either with the
wrong transport raises an error. This is backward compatible: existing
mcp.Client("https://…", namespace=…, bearer_token=…) calls are unchanged.
The JSON-RPC framing for both the stdio server and client is provided by the
reusable github.com/paularlott/jsonrpc
package via the github.com/paularlott/mcp
library.
re: re.sub() with count=0 now replaces all matches (Python semantics).
Previously, re.sub(pattern, repl, string, count=0) returned the string unchanged (zero replacements), diverging from Python where count=0 means “replace all”. It now matches Python: count=0 (the default) replaces all occurrences, and a positive count limits the number of replacements. This also makes sub consistent with re.split, which already treated maxsplit=0 as “no limit”.
scriptling.runtime.kv: incr() is now atomic.
kv.incr() performed a non-atomic read-then-write, so concurrent increments on the same key could lose updates. It now performs an atomic read-modify-write against the underlying store, so parallel increments are safe and no updates are lost.
runtime.plugin: expose a Scriptling script as a first-class plugin peer (agent variant only).
A Scriptling setup script can now implement the full Scriptling plugin protocol
so that host processes can load it with scriptling=True and receive
auto-generated plugin.<name> proxy libraries: no compiled binary required.
# setup.py
import scriptling.runtime.plugin as plugin_srv
import scriptling.runtime as runtime
plugin_srv.serve("calculator", "1.0", "Basic arithmetic")
plugin_srv.register_function("add", "handlers.add")
plugin_srv.register_function("multiply", "handlers.multiply")
runtime.start_server()# handlers.py : called on a fresh evaluator per request
def add(a, b):
return a + b
def multiply(a, b):
return a * bWhen a client loads this peer with scriptling=True, the host performs the
scriptling.handshake, receives the schema, and builds a plugin.calculator
proxy library with generated wrappers for add and multiply. Function
handlers receive individual positional arguments decoded from the plugin
transport (not a raw params blob).
runtime.plugin is registered only in the agent variant of Scriptling: it
is intentionally absent from the general CLI runtime. See the
Plugin Server Mode docs.
runtime.start_server(wait=True) and runtime.server_running(): keep the setup script alive while the server runs.
Previously, a setup script registered its routes and then exited; the server started automatically in the background. This made it impossible for the setup script to remain alive alongside the running server (to maintain gossip state, share objects, or run a polling loop that feeds HTTP handlers).
Now the setup script can call runtime.start_server() to signal that all routes are registered: the server builds its mux and starts listening: and then continue running for the lifetime of the server:
import scriptling.runtime as runtime
import scriptling.net.gossip as gossip
cluster = gossip.create(bind_addr="0.0.0.0:8001")
cluster.start()
# Register HTTP routes
runtime.http.get("/status", "handle_status")
# Signal the server to start, then stay alive while it runs
runtime.start_server(wait=False)
while runtime.server_running():
yield_now()Use wait=True (the default) to block inside start_server() until the server shuts down: equivalent to Flask’s app.run(). Use wait=False with a server_running() loop to keep the script running while also doing other work.
Backward compatibility: scripts that exit without calling start_server() continue to work unchanged: the server starts automatically after the setup script finishes.
The same pattern works for HTTP, JSON-RPC, and MCP servers.
Interpreter lock (GIL): one environment is now safe to use from many goroutines.
Each Scriptling environment now has an interpreter lock, acquired automatically whenever script runs. Any number of goroutines may call into the same environment: shared-state threads, background handlers, server requests: and the lock guarantees only one runs script at a time, so shared state can no longer be corrupted by concurrent access (no locks needed in your handlers). Independent environments keep independent locks and still run fully in parallel, so the per-request isolation used by the servers keeps its throughput.
Blocking calls release the lock while they wait, so other threads keep making
progress: sleeps, input(), file I/O, sockets, WebSocket, subprocess, HTTP, AI
completions/streaming, container calls, plugin calls, provisioning, wait_for,
grep/sed, messaging sends, runtime.sync primitives, Promise.wait(), and
gossip send_request(). For tight CPU-bound loops that never block, the new
global yield_now() builtin releases the lock briefly so other threads can run.
runtime.background(..., shared=True): shared-environment threads.
background() gains a keyword-only shared flag. With shared=True the handler
runs on a goroutine in the caller’s own environment, sharing its live
variables; the interpreter lock makes concurrent access safe without locks.
Arguments are passed live (no transferable restriction or copying). The default
(shared=False) is unchanged: an isolated, parallel copy. Also fixed docs that
referenced a non-existent runtime.spawn: the function is runtime.background.
scriptling.console handlers run one at a time, to completion:
on_submit, on_escape, and register_command handlers run sequentially: each
finishes before the next starts. This is for ordering, not safety (the
interpreter lock already makes them memory-safe): a chat conversation is only
coherent if a submit handler completes before the next begins, and serializing
keeps input responsive while a handler runs. A long-running handler delays the
next one; submitting new input or pressing Esc still cancels the in-flight
handler so cooperative handlers can stop early.
Faster interpreter hot path:
A set of interpreter optimizations cut per-call and per-statement overhead:
- A call-site callee cache resolves functions defined in an enclosing scope (recursion, shared helpers) without re-walking the environment chain.
- Call-frame environments are recycled through a per-tree free-list instead of a global pool.
- Return values are now recycled through the same per-tree free-list,
removing the per-
returnoverhead of a global pool while keeping allocations flat. - The source-file lookup used for error reporting is no longer performed on every block and statement: it is deferred to the error path, removing a context lookup from every loop body and function call. The call-depth lookup is likewise resolved in a single step per call.
- Hot type checks (
IsError, exception detection) use direct type assertions instead of interface method calls. - Integer dictionary keys (loop indices, ids) allocate far less: small keys are now cached and reused, and larger keys take a single allocation instead of two. Dict-heavy code does roughly 40% fewer allocations.
- Object instances store their fields inline instead of always allocating a map. Most instances have only a handful of fields, so this avoids a heap map (and its buckets) per object entirely; fields beyond a small inline capacity spill to a map as before. Class-heavy code runs about 13% faster and uses roughly 23% less memory.
Recursion-heavy workloads (e.g. recursive fib) run roughly 16% faster,
dict-heavy code about 15% faster, class-heavy code about 13% faster, and
loop- and call-heavy code is broadly faster too: with fewer allocations across
the board.
Breaking change (Go embedders / plugin authors only): instance fields are now accessed through methods, not a map.
object.Instance no longer exposes a public Fields map[string]Object. Go code
that read or wrote instance fields directly must switch to the accessor methods:
// Before
v := inst.Fields["name"]
inst.Fields["name"] = object.NewString("x")
delete(inst.Fields, "name")
// After
v, ok := inst.GetField("name") // or inst.Field("name") for a bare value
inst.SetField("name", object.NewString("x"))
inst.DeleteField("name")Also available: HasField, FieldCount, RangeFields, and FieldsSnapshot,
plus the constructors object.NewInstance(class),
object.NewInstanceWithFields(class, fields) and
object.NewInstanceWithData(class, fields, nativeData). This decouples the
public API from the storage layout, which is what enabled the inline-field
optimization above. Scripts are unaffected: this only touches Go code that
manipulates instances directly.
Embeddable server: inject host libraries via ServerConfig.ExtraLibs
The server runtime (scriptling-cli/server) now accepts an optional
ExtraLibs func(*scriptling.Scriptling) on ServerConfig. When set, it runs on
every evaluator the server builds: the setup script and every JSON-RPC, HTTP,
MCP, and WebSocket request handler: after the standard library set. Host
applications embedding the server can use it to expose their own libraries to
served scripts:
cfg := server.ServerConfig{
ScriptFile: "setup.py",
ExtraLibs: func(p *scriptling.Scriptling) {
myapp.RegisterLibraries(p)
},
}
server.RunJSONRPCServer(ctx, cfg)Breaking change: hashlib now returns hash objects:
hashlib.md5(), hashlib.sha1() and hashlib.sha256() previously returned a
hexadecimal string directly. They now return hash objects, matching Python.
Call .hexdigest() (or .digest()) on the result:
# Before (v0.12)
h = hashlib.sha256("hello") # a hex string
# After (v0.14)
h = hashlib.sha256("hello").hexdigest()The hash objects also support .update(data), .copy(), and the .name,
.digest_size and .block_size attributes. Constructors accept an optional
initial data argument (a string used as a byte buffer, or a list of byte
values such as returned by str.encode()).
New hmac standard library (alongside hashlib):
hmac.new(key, msg=None, digestmod=None)returns an HMAC object with.update(),.digest(),.hexdigest(),.copy()and.name/.digest_size/.block_sizeattributes.digestmodaccepts a string name ("sha256"default,"sha1","md5"), the omitted value, or ahashlibconstructor reference (hashlib.sha256).hmac.digest(key, msg, digestmod)is a one-shot helper.hmac.compare_digest(a, b)performs a constant-time string comparison.
This makes webhook signature verification (GitHub, Stripe, Slack, etc.) a one-liner:
import hmac, hashlib
def verify(body, signature, secret):
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)secrets.compare_digest now delegates to the shared hmac.compare_digest
implementation. Note that scriptling has no bytes type: strings are used as
byte buffers and parameter type annotations are not supported, so write
def verify(body, signature, secret):.
Plugin Manager Scoping:
manager.NewScope(opts ...ScopeOption)creates a child manager that isolates dynamically loaded plugins from the parent and from sibling scopes.- Scopes chain for lookup:
GetandListfall back to the parent (and so on up the chain) when a name is not found locally. A child scope may not load a plugin under a name that already exists in any ancestor: attempting this returns an error. Loading the exact same endpoint under the same name is idempotent and returns the ancestor’s client unchanged. scope.Close()shuts down and unloads only the scope-local plugins. The parent and sibling scopes are completely unaffected. The local client map is cleared atomically on close.scope.Unload(name)removes only scope-local plugins; parent plugins cannot be unloaded from a child scope.- Scopes can be nested to arbitrary depth.
Get,List, and transport inheritance propagate correctly through the full chain. WithTransport(TransportHTTP)restricts a scope to HTTP(S) endpoints only: any attempt to load a stdio executable raises an error.WithTransport(TransportStdio)restricts a scope to stdio executables only: any attempt to load an HTTP(S) URL raises an error.WithTransport(TransportAll)(default) permits both transport types.
Pooled, HTTP/2-capable transports:
plugin.NewManagernow creates two shared*http.Transportinstances: one TLS-verified, one with TLS skip-verify: that are inherited by all child scopes viaNewScope. Connections to the same endpoint are reused across scopes, reducing TLS handshake overhead and enabling HTTP/2 connection reuse.manager.LoadURL(ctx, name, url, scriptling, insecureSkipTLS=true, ...)now draws from the manager’s pooled skip-verify transport rather than creating a new transport per connection.- The shared transports use
ForceAttemptHTTP2: true,MaxIdleConnsPerHost: 20, andIdleConnTimeout: 90s.
Markdown Library:
scriptling.markdown.to_htmlno longer passes raw HTML through to the output. Raw HTML blocks and inline HTML are now dropped (replaced with an HTML comment), so untrusted input such as LLM output cannot inject<script>tags, event handlers, or other dangerous markup. The rendered HTML is safe for direct insertion into a page. HTML-like syntax inside code spans and fenced code blocks is still rendered, with angle brackets HTML-escaped.
Plugins with scriptling.plugin:
scriptling.plugin.load(...)can now load local executables and HTTP(S) JSON-RPC endpoints.- Works with both plugin styles: typed Scriptling plugin protocol (
scriptling=True) and raw JSON-RPC methods (scriptling=False, default). - New
scriptling.plugin.batch_call(...)supports batched calls with ordered results. describe()/list()include handshake metadata for Scriptling plugins, while raw JSON-RPC peers skip handshake metadata.- Loading is deduplicated by absolute path; conflicting name/path combinations raise an error.
- New
scriptling.plugin.unload(name)cleanly stops and removes a plugin. - Go manager support matches this behavior, including HTTP(S) loading.
scriptling.pluginis always available in run, server, and--json-rpcmodes (even without--plugin-dir).
JSON-RPC over HTTP:
scriptling --server ... --json-rpc ...now exposes JSON-RPC atPOST /json-rpc.scriptling --json-rpc ...still works over stdio when--serveris not used.- HTTP JSON-RPC supports single calls, batches, and notifications (
204 No Contentfor notification-only requests), and can run alongside existing HTTP routes and MCP tools.
Provisioning Library: Managed Blocks:
- New
scriptling.provision.file.ensure_block(...)manages marker-delimited sections inside a file, updating only the managed block and leaving the rest untouched. - Supports start/end placement, insertion after a matching line, and multiple independent blocks via unique block ids.
- New
scriptling.provision.file.absent_block(...)removes a managed block and returnsfile.REMOVEDorfile.UNCHANGED.
Provisioning Library: Fetch:
- New
scriptling.provision.fetch.file(...)downloads HTTP/HTTPS files idempotently, with optional size limits and zip extraction. - Supports trusted internal endpoints (
insecure=True) and optionalprovideschecks to skip work when expected files already exist. - Zip extraction is safe against path traversal and keeps executable bits.
Provisioning Library File:
scriptling.provision.file.ensurenow accepts acreate_only=Falsekeyword argument; whenTrue, an existing file is never modified and the call returnsfile.UNCHANGED, while new files are still written normally: useful for seeding config files only on first run
JSON-RPC stdio server:
- Added
scriptling.runtime.jsonrpc, a concurrent stdin/stdout JSON-RPC 2.0 server. Handlers are referenced by string ("library.function") and run on a fresh, isolated evaluator per request. Supports batches, notifications, and structured errors. Start withscriptling --json-rpc setup.py; logs are redirected to stderr so the stdout stream stays pure JSON-RPC.
Markdown Library:
- New
scriptling.markdownlibrary withto_html(markdown_string)for converting Markdown to HTML
Language: ImportError handling:
- Failed
importandfrom ... import ...statements now raise catchableImportErrorexceptions insidetry/except, while uncaught import failures still terminate evaluation as before ImportError(...)is available as a built-in exception constructor and can be used in typedexcept ImportErrorand tupleexcept (..., ImportError)clauses
Plugin System:
- Plugin class properties are exposed through generated and native proxies, including read-only getters and getter/setter pairs
- Go plugins can emit records to the host logger with
plugin.Logger(ctx); host applications pass the manager-lifetime logger toplugin.NewManager(appLogger, crashHandler) - Plugin manager crash handling can be supplied during construction, with
SetCrashHandler()still available for late wiring - New runnable plugin examples for properties and host logging
Version:
- Scriptling version bumped to
v0.11.0
Plugin System:
- Go plugin system for extending Scriptling with out-of-process plugins written in Go
plugin.NewServerfor building plugin executables that register functions, classes, and constants via RPCplugin.Managerfor loading and managing plugin binaries from directories--plugin-dirCLI flag andSCRIPTLING_PLUGIN_DIRenv var for loading plugins in both CLI and server modes- Auto-generated proxy classes with
__del__that releases plugin-side objects on garbage collection - GC finalizers installed automatically on all class instances with
__del__: both in-process and plugin objects are cleaned up when unreachable - Custom wrapper class source for full control over the host-side proxy
scriptling.plugincontrol library:list(),describe(),call_function(),call_method(),release()- Concurrent plugin server: multiple requests are dispatched in parallel so slow calls from one environment don’t block another
plugin.RegisterScriptFunc/plugin.RegisterScriptClassfor host-side Scriptling code within plugin libraries- Plugin callbacks: plugins can call host-provided functions via
plugin.Callbackinterface during RPC calls, enabling streaming, event handlers, and other bidirectional patterns object.GCReleaseHookfor best-effort cleanup when GC collects plugin-owned objects- Evaluator installs
runtime.SetFinalizeron instances of classes that define__del__, calling the destructor automatically on garbage collection
Class Builder:
object.ClassBuilder.Constructor(fn)for typed receiver classes: constructor returns a Go struct, methods receive it directlyobject.ClassBuilder.Property(name, getter)andPropertyWithSetter(name, getter, setter)for exposing private Go struct fields as readable/writable Scriptling properties
AI Library: Vector Similarity:
- New
ai.cosine_similarity(a, b)for comparing two embedding vectors; returns a score from -1.0 to 1.0
Networking Library: DNS Resolve:
- New
scriptling.net.resolvelibrary withlookup_ip,lookup_srv, andresolve_srv_httpfor DNS resolution
Provisioning Library: File:
- New
scriptling.provision.filelibrary withensure(path, content, mode=0o644)for idempotent file provisioning
MCP Server: tool.return_error() messages lost:
- When a tool called
return_error(), the error message was discarded and replaced with a genericTool execution failed: script execution failed: SystemExit: 1; the actual error message from the script is now correctly returned to the MCP client
MCP Server: Tool script changes not auto-reloading:
- The file watcher only triggered a reload on
.tomlchanges; editing a tool’s.pyscript now also triggers an automatic reload
CLI: Server Logging:
- The HTTP and MCP servers now emit
trace/debug-level diagnostics for incoming requests, dispatched handlers, middleware execution, MCP tool invocations, and WebSocket lifecycle events, controlled by--log-level/SCRIPTLING_LOG_LEVELand--log-format/SCRIPTLING_LOG_FORMAT
Docs: AI Client reference split:
- The AI Client method reference (completion, streaming, embeddings, Responses API, Pipeline, etc.) has been split into its own Client page for easier navigation
Filesystem Libraries: Pathlib copy, rename, iterdir, glob:
- New
pathlib.Path.copy(target)to copy a file or directory tree to a target path, returning a newPathpointing to the target - New
pathlib.Path.rename(target)to rename or move a file or directory, returning a newPathpointing to the target - New
pathlib.Path.iterdir()to list directory contents asPathobjects - New
pathlib.Path.glob(pattern)to match a shell-style wildcard pattern (*,?,**) within a directory and return matchingPathobjects
Language: Constant Folding:
- Constant expressions (arithmetic, string concatenation, comparisons on literals) are now evaluated at compile time, reducing runtime overhead
Language: Keyword-only Parameters:
- Function and lambda signatures now support Python-style keyword-only parameters using a bare
*, such asdef resize(image, *, width, height=100): - Keyword-only parameters are enforced at call time, so values after bare
*must be supplied by name - Keyword-only parameters can also follow
*args, such asdef collect(prefix, *items, sep):
Language: Shorthand operators:
- Shorthand assignment operators (
+=,-=,*=,/=,//=,%=,**=,&=,|=,^=,>>=,<<=) now correctly evaluate the left-hand side expression against objects.
AI Library: Parallel Completions:
- New
client.completion_parallel(model, messages_list, max_parallel=1, **kwargs): run multiple chat completions concurrently - New
client.ask_parallel(model, messages_list, max_parallel=1, **kwargs): run multiple ask completions concurrently - The
max_parallelparameter controls the number of concurrent API requests (default: 1, sequential)
AI Library: Automatic Retry & Adaptive Parallel Concurrency:
ai.Client()now acceptsmax_retries(default 3),retry_backoff(default 1.0s),retry_on_rate_limit(default True), andretry_on_server_error(default True) kwargs- 429 (rate limit) and 5xx (server error) responses are automatically retried with exponential backoff
- Completion responses now include a
retrydict when retries occurred:{"attempts": 2, "rate_limit_hit": true, "total_backoff": 1.0} completion_parallel()andask_parallel()now use adaptive concurrency: parallelism is automatically halved when rate limits are detected, reducing API pressure without manual tuning; workers wake immediately when backoff expires
AI Library: Pipeline:
- New
client.Pipeline(model, max_parallel=1, ask=False, **kwargs): creates a streaming completion pipeline that starts processing requests immediately as they are added, overlapping prompt generation with inference pipe.add(message): queues a message; inference begins immediately as concurrency slots are availablepipe.complete(): waits for all in-flight requests and returns results ordered by submissionask=Truemode returns plain text strings;ask=False(default) returns full response dictscompletion_parallel()andask_parallel()are now implemented on top of Pipeline internally, giving them the same overlap benefit when items are added faster than they complete
Requests Library: Parallel HTTP Requests:
- New
requests.parallel(requests, max_parallel=4): execute multiple HTTP requests concurrently and return responses in submission order - Each request is a dict with
method,url, and optionaldata,json,headers,params,auth,timeout - Returns a list of
Responseobjects (or objects with anerrorattribute on failure) - The
max_parallelparameter controls the concurrency limit (default: 4)
Filesystem Libraries: Permission Modes:
- New
os.chmod(path, mode)andpathlib.Path.chmod(mode)for changing file or directory permissions os.mkdir(path, mode=0o777),os.makedirs(path, mode=0o777, exist_ok=False), andpathlib.Path.mkdir(mode=0o777, parents=False, exist_ok=False)now accept Python-style mode arguments for directory creation- New
os.removedirs(name)for pruning empty directory trees - New
pathlib.Path.read_bytes()andpathlib.Path.write_bytes(data)for byte-oriented file helpers os.write_file(path, content, mode=0o644)now accepts an optional mode for newly-created filesfs.write_bytes(path, offset, data, mode=0o644)now accepts an optional mode for newly-created files
AI Library: Removed tool_round and tool_round_parallel:
- Removed
ai.tool_round(): useclient.completion()withai.tool_calls()andai.execute_tool_calls()directly, or use theAgentclass for full tool loops - Removed
ai.tool_round_parallel(): useclient.completion_parallel()withai.execute_tool_calls()directly