Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ permissions:
contents: read

env:
# The submilli-server release the integration tests run against.
SUBMILLI_VERSION: "0.4.1"
# Batch documentation requires this runtime commit until its next release.
SUBMILLI_RUNTIME_REF: "1f1377033809fe459354f7ec8f1787cd1fe5e845"

jobs:
lint:
Expand Down Expand Up @@ -77,20 +77,22 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.0
- name: Download submilli-server ${{ env.SUBMILLI_VERSION }}
run: |
mkdir -p .submilli-bin && cd .submilli-bin
base=https://github.com/submilli/submilli-runtime/releases/download/v${SUBMILLI_VERSION}
curl -fsSLO "$base/submilli-server-x86_64-unknown-linux-musl"
curl -fsSLO "$base/SHA256SUMS"
grep ' submilli-server-x86_64-unknown-linux-musl$' SHA256SUMS | sha256sum -c
mv submilli-server-x86_64-unknown-linux-musl submilli-server
chmod +x submilli-server
- uses: actions/checkout@v7
with:
repository: submilli/submilli-runtime
ref: ${{ env.SUBMILLI_RUNTIME_REF }}
path: runtime
- uses: dtolnay/rust-toolchain@1.98.1
- name: Build the runtime with batch documentation
run: cargo build --locked --release -p submilli-server
working-directory: runtime
env:
CARGO_BUILD_JOBS: "2"
- run: uv sync --all-packages --all-groups --locked
- run: uv run pytest ${{ matrix.tests }} -m 'not durable'
working-directory: packages/${{ matrix.package }}
env:
SUBMILLI_BIN: ${{ github.workspace }}/.submilli-bin
SUBMILLI_BIN: ${{ github.workspace }}/runtime/target/release

durable-integration:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions packages/langchain-submilli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- SUB-2074: Fetch package documentation in one ordered batch. The SDK adds `package_docs_batch(id, names)` on blueprints and sessions; `package_docs(id, name)` retains its existing GET endpoint and behavior. Agent documentation tools now take `names: string[]`. Batch calls require a server with SUB-2074 support.


All notable changes to this project are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
[Semantic Versioning](https://semver.org/).
Expand Down
2 changes: 1 addition & 1 deletion packages/langchain-submilli/examples/research/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "submilli:fs";

function main(): string {
const before: string[] = [];
for (const entry of fs.list(".", false)) before.push(entry.name);
for (const entry of fs.list(".")) before.push(entry.name);
fs.writeText("check.md", "# Written by a check\n");
return `notes before: ${before.join(", ") || "none"}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def prompt(self) -> str:
"\n\nThis agent's programs can also call the application's own tools, as the `@tools` package "
f'(`import tools from "@tools";`): {names}. The application runs each call and returns its '
f"result to the program.{approval} Read their signatures with "
'`submilli__typescript__packages__docs("@tools")`.'
'`submilli__typescript__packages__docs({"names":["@tools"]})`.'
)

async def answer(self, call: PendingCall, runtime: ToolRuntime[Any, Any], used: list[int]) -> CallOutcome:
Expand Down
20 changes: 17 additions & 3 deletions packages/langchain-submilli/src/langchain_submilli/_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,26 @@ async def search(session_id: str) -> str:

return await self._in_session(t.PACKAGES_SEARCH, runtime, search)

async def _packages_docs(self, name: str, runtime: ToolRuntime) -> str | ToolMessage | Command[Any]:
async def _packages_docs(self, names: list[str], runtime: ToolRuntime) -> str | ToolMessage | Command[Any]:
if not self._exposed:
return (await self.client.blueprints.package_docs(self.blueprint, name)).text
return t.as_text(
{
"results": [
dict(name=e.name, found=e.found, text=e.text)
for e in await self.client.blueprints.package_docs_batch(self.blueprint, names)
]
}
)

async def docs(session_id: str) -> str:
return (await self.client.sessions.package_docs(session_id, name)).text
return t.as_text(
{
"results": [
dict(name=e.name, found=e.found, text=e.text)
for e in await self.client.sessions.package_docs_batch(session_id, names)
]
}
)

return await self._in_session(t.PACKAGES_DOCS, runtime, docs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class PackagesSearchArgs(BaseModel):


class PackagesDocsArgs(BaseModel):
name: str = Field(description="Package name, e.g. `submilli:crypto`.")
names: list[str] = Field(description="Package names to read before writing a program.")


class BuiltinsDocsArgs(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ async def test_a_program_calls_a_typed_tool_and_uses_its_fields(managed: Managed


async def test_package_docs_show_the_tools_signatures(managed: ManagedServer) -> None:
graph = agent(managed, [call("submilli__typescript__packages__docs", {"name": "@tools"})], "done")
graph = agent(managed, [call("submilli__typescript__packages__docs", {"names": ["@tools"]})], "done")
text = first_tool(await start(graph)).content
assert "function query_orders(args: { customer: string }): { count: number; for_user: number };" in text, text

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ async def test_discovery_tools(submilli: SubmilliMiddleware) -> None:
model = scripted(
[
call("submilli__typescript__packages__search", {"query": "jina"}),
call("submilli__typescript__packages__docs", {"name": "@submilli/jina"}),
call("submilli__typescript__packages__docs", {"names": ["@submilli/jina"]}),
call("submilli__typescript__builtins__list", {}),
call("submilli__typescript__builtins__docs", {"names": ["Map", "Temporal.Instant"]}),
],
Expand Down
21 changes: 21 additions & 0 deletions packages/langchain-submilli/tests/unit_tests/fake_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ def handle(self, request: httpx.Request) -> httpx.Response:
return _json(401, {"error": "unauthorized", "message": "missing or unrecognised API token"})
path, method = request.url.path, request.method
body: dict[str, Any] = json.loads(request.content) if request.content else {}
if (
method == "GET"
and "name" not in request.url.params
and re.fullmatch(r"/v1/(sessions|blueprints)/[^/]+/packages/docs", path)
):
scope = httpx.Request(
"GET",
request.url.copy_with(query=None).copy_set_param("name", "@submilli/jina"),
headers=request.headers,
)
exists = self.handle(scope)
if exists.status_code != 200:
return exists
results = []
for name in request.url.params.get_list("names[]"):
single = httpx.Request(
"GET", request.url.copy_with(query=None).copy_set_param("name", name), headers=request.headers
)
response = self.handle(single)
results.append({"name": name, "found": response.status_code == 200, "text": response.text})
return httpx.Response(200, json={"results": results})
if method == "POST" and path == "/v1/sessions":
return self.open(body)
if match := re.fullmatch(r"/v1/sessions/([^/]+)/execute", path):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ async def test_the_execute_description_tells_the_model_about_tools(server: FakeS
[described] = [tool for tool in model.bound[-1] if tool.name == "submilli__typescript__execute"]
assert '`import tools from "@tools";`' in described.description
assert "`query_orders`, `send_email`" in described.description
assert 'packages__docs({"names":["@tools"]})' in described.description
assert "approval" in described.description
# Exposed tools are for programs; the model is not given them.
assert "query_orders" not in model.tool_names()
Expand Down Expand Up @@ -346,7 +347,7 @@ async def test_a_changed_tool_catalog_opens_a_new_session(server: FakeServer) ->


async def test_package_docs_include_the_sessions_tools(server: FakeServer) -> None:
model = calls_then_done(call("submilli__typescript__packages__docs", {"name": "@tools"}))
model = calls_then_done(call("submilli__typescript__packages__docs", {"names": ["@tools"]}))
result = await start(agent(model, middleware(ptc=[query_orders])))

assert server.calls("GET", r"/v1/sessions/.*/packages/docs")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ async def test_different_variables_on_a_thread_are_refused(server: FakeServer) -
async def test_discovery_tools_do_not_open_a_session(server: FakeServer) -> None:
model = calls_then_done(
call("submilli__typescript__packages__search", {"query": "jina"}),
call("submilli__typescript__packages__docs", {"name": "@submilli/jina"}),
call("submilli__typescript__packages__docs", {"name": "nope"}),
call("submilli__typescript__packages__docs", {"names": ["@submilli/jina"]}),
call("submilli__typescript__packages__docs", {"names": ["nope"]}),
call("submilli__typescript__builtins__list", {}),
call("submilli__typescript__builtins__docs", {"names": ["Map", "JSON"]}),
call("submilli__typescript__last_run", {}),
Expand All @@ -138,7 +138,7 @@ async def test_discovery_tools_do_not_open_a_session(server: FakeServer) -> None
outputs = [m.content for m in result["messages"] if m.type == "tool"]
assert json.loads(outputs[0])["results"][0]["name"] == "@submilli/jina"
assert "searchJson" in outputs[1]
assert json.loads(outputs[2])["error"] == "unknown_package"
assert json.loads(outputs[2])["results"][0]["found"] is False
assert json.loads(outputs[3])["types"] == ["Map"]
assert [r["name"] for r in json.loads(outputs[4])["results"]] == ["Map", "JSON"]
assert outputs[5] == "No program has run in this thread yet."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class TestPackagesSearch(_Base):

class TestPackagesDocs(_Base):
name = "submilli__typescript__packages__docs"
example: ClassVar[dict[str, Any]] = {"name": "@submilli/jina"}
example: ClassVar[dict[str, Any]] = {"names": ["@submilli/jina"]}


class TestBuiltinsList(_Base):
Expand Down
5 changes: 5 additions & 0 deletions packages/openai-agents-submilli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- SUB-2074: Fetch package documentation in one ordered batch. The SDK adds `package_docs_batch(id, names)` on blueprints and sessions; `package_docs(id, name)` retains its existing GET endpoint and behavior. Agent documentation tools now take `names: string[]`. Batch calls require a server with SUB-2074 support.


All notable changes to this project are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
[Semantic Versioning](https://semver.org/).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def prompt(self) -> str:
"\n\nThis agent's programs can also call the application's own tools, as the `@tools` package "
f'(`import tools from "@tools";`): {names}. The application runs each call and returns its '
f"result to the program.{approval} Read their signatures with "
'`submilli__typescript__packages__docs("@tools")`.'
'`submilli__typescript__packages__docs({"names":["@tools"]})`.'
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,26 @@ async def search(session_id: str) -> str:
return await self._in_session(context, search)

async def _packages_docs(self, context: ToolContext[Any], arguments: dict[str, Any]) -> str:
name = str(arguments["name"])
names = arguments["names"]
if not self._program_tools:
return (await self.client.blueprints.package_docs(self.blueprint, name)).text
return t.as_text(
{
"results": [
dict(name=e.name, found=e.found, text=e.text)
for e in await self.client.blueprints.package_docs_batch(self.blueprint, names)
]
}
)

async def docs(session_id: str) -> str:
return (await self.client.sessions.package_docs(session_id, name)).text
return t.as_text(
{
"results": [
dict(name=e.name, found=e.found, text=e.text)
for e in await self.client.sessions.package_docs_batch(session_id, names)
]
}
)

return await self._in_session(context, docs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ def _object(properties: dict[str, Any]) -> dict[str, Any]:
}
}
)
PACKAGES_DOCS_PARAMS = _object({"name": {"type": "string", "description": "Package name, e.g. `submilli:crypto`."}})
PACKAGES_DOCS_PARAMS = _object(
{
"names": {
"type": "array",
"items": {"type": "string"},
"description": "Package names to read before writing a program.",
}
}
)
BUILTINS_DOCS_PARAMS = _object(
{
"names": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async def test_a_program_calls_a_typed_tool_with_the_runs_context(managed: Manag


async def test_package_docs_show_the_tools_signatures(managed: ManagedServer) -> None:
model = calls_then_done(call("submilli__typescript__packages__docs", {"name": "@tools"}))
model = calls_then_done(call("submilli__typescript__packages__docs", {"names": ["@tools"]}))
[output] = tool_outputs(await Runner.run(shop(managed, model), "go", context=ada()))
assert "function query_orders(args: { customer: string }): { count: number; for_user: number };" in output.output

Expand Down
21 changes: 21 additions & 0 deletions packages/openai-agents-submilli/tests/unit_tests/fake_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ def handle(self, request: httpx.Request) -> httpx.Response:
return _json(401, {"error": "unauthorized", "message": "missing or unrecognised API token"})
path, method = request.url.path, request.method
body: dict[str, Any] = json.loads(request.content) if request.content else {}
if (
method == "GET"
and "name" not in request.url.params
and re.fullmatch(r"/v1/(sessions|blueprints)/[^/]+/packages/docs", path)
):
scope = httpx.Request(
"GET",
request.url.copy_with(query=None).copy_set_param("name", "@submilli/jina"),
headers=request.headers,
)
exists = self.handle(scope)
if exists.status_code != 200:
return exists
results = []
for name in request.url.params.get_list("names[]"):
single = httpx.Request(
"GET", request.url.copy_with(query=None).copy_set_param("name", name), headers=request.headers
)
response = self.handle(single)
results.append({"name": name, "found": response.status_code == 200, "text": response.text})
return httpx.Response(200, json={"results": results})
if method == "POST" and path == "/v1/sessions":
return self.open(body)
if match := re.fullmatch(r"/v1/sessions/([^/]+)/execute", path):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ async def test_a_program_calls_the_applications_tools(server: FakeServer) -> Non
# The model sees the tools in the execute description, not as tools of its own.
described = model.descriptions()
assert "`lookup_order`, `customer`" in described["submilli__typescript__execute"]
assert 'packages__docs({"names":["@tools"]})' in described["submilli__typescript__execute"]
assert "lookup_order" not in described


Expand Down Expand Up @@ -217,7 +218,7 @@ async def test_two_approvals_resume_from_a_serialized_state(server: FakeServer)
async def test_discovery_includes_the_sessions_tools(server: FakeServer) -> None:
model = calls_then_done(
call("submilli__typescript__packages__search", {"query": ""}),
call("submilli__typescript__packages__docs", {"name": "@tools"}),
call("submilli__typescript__packages__docs", {"names": ["@tools"]}),
)
result = await Runner.run(agent(toolkit(greet), model), "go", context=ADA)
texts = [item.output for item in result.new_items if isinstance(item, ToolCallOutputItem)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,15 @@ class Context:
async def test_discovery_tools_do_not_open_a_session(server: FakeServer) -> None:
model = calls_then_done(
call("submilli__typescript__packages__search", {"query": "jina"}),
call("submilli__typescript__packages__docs", {"name": "@submilli/jina"}),
call("submilli__typescript__packages__docs", {"names": ["@submilli/jina"]}),
call("submilli__typescript__builtins__list", {}),
call("submilli__typescript__builtins__docs", {"names": ["Map"]}),
call("submilli__typescript__last_run", {}),
)
result = await run(toolkit(), model)
texts = [item.output for item in outputs(result)]
assert json.loads(texts[0])["results"][0]["name"] == "@submilli/jina"
assert texts[1].startswith("# jina")
assert json.loads(texts[1])["results"][0]["text"].startswith("# jina")
assert texts[4] == "No program has run in this conversation yet."
assert server.sessions == {}

Expand Down
7 changes: 7 additions & 0 deletions packages/submilli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

Release coordination: publish the updated core SDK before its integrations and raise their SDK dependency minimums to that release. These PRs do not publish packages.

- SUB-2074: Fetch package documentation in one ordered batch. The SDK adds `package_docs_batch(id, names)` on blueprints and sessions; `package_docs(id, name)` retains its existing GET endpoint and behavior. Agent documentation tools now take `names: string[]`. Batch calls require a server with SUB-2074 support.


All notable changes to this project are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
[Semantic Versioning](https://semver.org/).
Expand Down
7 changes: 7 additions & 0 deletions packages/submilli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ await client.blueprints.builtin_docs("research", ["Map", "Temporal"])
`sessions.search_packages` and `sessions.package_docs` answer the same for one session, including
its `@tools`.

### Batch package documentation

Call `package_docs_batch(id, names)` on `client.blueprints` or `client.sessions` to fetch several modules in one request. Batch requests use GET with repeated `names[]` query parameters. Results retain input order and duplicates; each entry has `name`, `found`, and `text`. An unknown package has `found: false`, while an unknown blueprint or session fails the request. An empty list returns an empty list.

The existing `package_docs(id, name)` API remains available and continues using the original GET route. Batch methods and the agent integrations require a server with SUB-2074 support; there is no fallback to individual requests.


## Errors

Every error is a `SubmilliError`:
Expand Down
2 changes: 2 additions & 0 deletions packages/submilli/src/submilli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ExecuteResult,
LastRun,
PackageDocs,
PackageDocsEntry,
PendingCall,
RunError,
RunningRun,
Expand Down Expand Up @@ -72,6 +73,7 @@
"LastRun",
"NotFoundError",
"PackageDocs",
"PackageDocsEntry",
"PendingCall",
"RunError",
"RunNotSuspendedError",
Expand Down
Loading
Loading