Skip to content

Commit 2a9eab8

Browse files
tylergibbs1claude
andcommitted
Bump to v0.11.0, add code mode example and WorkerExecutor edge case tests
- Add real-world code mode example using public APIs (Open-Meteo, RestCountries) - Add 15 new WorkerExecutor edge case tests (119 total codemode tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 709a40b commit 2a9eab8

3 files changed

Lines changed: 349 additions & 1 deletion

File tree

examples/05-code-mode.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* Code Mode example — the LLM writes code that orchestrates multiple API tools
3+
* in a single execution, instead of making individual tool calls with round-trips.
4+
*
5+
* Uses free public APIs (no keys needed):
6+
* - Open-Meteo for weather data
7+
* - RestCountries for country info
8+
* - IP-API for geolocation
9+
*
10+
* Run: bun examples/05-code-mode.ts
11+
*/
12+
13+
import { z } from "zod";
14+
import { Agent, AzureChatCompletionsModel, stream, tool } from "../src";
15+
import { createCodeModeTool, WorkerExecutor } from "../src/core/codemode";
16+
17+
// ── Model ──────────────────────────────────────────────────────────
18+
19+
const model = new AzureChatCompletionsModel({
20+
endpoint: process.env.AZURE_OPENAI_ENDPOINT!,
21+
apiKey: process.env.AZURE_OPENAI_API_KEY!,
22+
deployment: process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-5-chat",
23+
apiVersion: process.env.AZURE_OPENAI_API_VERSION,
24+
});
25+
26+
// ── Tools (real APIs) ──────────────────────────────────────────────
27+
28+
const getWeather = tool({
29+
name: "get_weather",
30+
description:
31+
"Get current weather for a location by latitude and longitude. Returns temperature (°C), wind speed, and weather description.",
32+
parameters: z.object({
33+
latitude: z.number().describe("Latitude of the location"),
34+
longitude: z.number().describe("Longitude of the location"),
35+
}),
36+
execute: async (_ctx, { latitude, longitude }) => {
37+
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,wind_speed_10m,weather_code`;
38+
const res = await fetch(url);
39+
const data = await res.json();
40+
return JSON.stringify(data.current);
41+
},
42+
});
43+
44+
const getCountryInfo = tool({
45+
name: "get_country_info",
46+
description:
47+
"Get information about a country by name. Returns capital, population, region, languages, and currencies.",
48+
parameters: z.object({
49+
country: z.string().describe("Country name (e.g. 'France', 'Japan')"),
50+
}),
51+
execute: async (_ctx, { country }) => {
52+
const url = `https://restcountries.com/v3.1/name/${encodeURIComponent(country)}?fields=name,capital,population,region,subregion,languages,currencies,latlng`;
53+
const res = await fetch(url);
54+
if (!res.ok) return JSON.stringify({ error: `Country "${country}" not found` });
55+
const data = await res.json();
56+
const c = data[0];
57+
return JSON.stringify({
58+
name: c.name.common,
59+
capital: c.capital?.[0],
60+
population: c.population,
61+
region: c.region,
62+
subregion: c.subregion,
63+
languages: c.languages,
64+
currencies: c.currencies,
65+
latlng: c.latlng,
66+
});
67+
},
68+
});
69+
70+
const geocode = tool({
71+
name: "geocode",
72+
description: "Convert a city/place name to latitude and longitude coordinates.",
73+
parameters: z.object({
74+
query: z.string().describe("City or place name to geocode (e.g. 'Paris', 'Tokyo')"),
75+
}),
76+
execute: async (_ctx, { query }) => {
77+
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1`;
78+
const res = await fetch(url);
79+
const data = await res.json();
80+
if (!data.results?.length) return JSON.stringify({ error: `Location "${query}" not found` });
81+
const r = data.results[0];
82+
return JSON.stringify({
83+
name: r.name,
84+
country: r.country,
85+
latitude: r.latitude,
86+
longitude: r.longitude,
87+
});
88+
},
89+
});
90+
91+
// ── Code Mode setup ────────────────────────────────────────────────
92+
93+
const executor = new WorkerExecutor({ timeout: 30_000 });
94+
95+
const codemode = createCodeModeTool({
96+
tools: [getWeather, getCountryInfo, geocode],
97+
executor,
98+
});
99+
100+
// ── Agent ──────────────────────────────────────────────────────────
101+
102+
const agent = new Agent({
103+
name: "travel-researcher",
104+
instructions: `You are a travel research assistant. When asked about destinations, use code mode to efficiently gather all the information in one go.
105+
106+
You have access to geocoding, weather, and country info APIs through the execute_code tool. Write code that calls multiple APIs in parallel using Promise.all when possible.
107+
108+
Always return structured, readable results.`,
109+
model,
110+
tools: [codemode],
111+
});
112+
113+
// ── Run ────────────────────────────────────────────────────────────
114+
115+
async function main() {
116+
const prompt =
117+
"Compare Paris, Tokyo, and New York City as travel destinations right now. For each city, get the current weather and country info. Tell me which one has the nicest weather today.";
118+
119+
console.log(`\n🔵 Prompt: ${prompt}\n`);
120+
console.log("─".repeat(60));
121+
console.log("Running with WorkerExecutor (isolated V8 context)...\n");
122+
123+
const { stream: s, result } = stream(agent, prompt, {
124+
maxTurns: 5,
125+
runHooks: {
126+
onToolStart: ({ toolName }) => {
127+
console.log(`\n🔧 Tool call: ${toolName}`);
128+
},
129+
onToolEnd: ({ toolName, result: toolResult }) => {
130+
console.log(`✅ ${toolName} completed`);
131+
try {
132+
const parsed = JSON.parse(toolResult);
133+
if (parsed.code) {
134+
// Code mode result — show the generated code
135+
console.log("\n📜 Generated code:");
136+
console.log("┌" + "─".repeat(78) + "┐");
137+
for (const line of parsed.code.split("\n")) {
138+
console.log(`│ ${line.padEnd(77)}│`);
139+
}
140+
console.log("└" + "─".repeat(78) + "┘");
141+
// Show execution result
142+
console.log("\n📦 Execution result:");
143+
const pretty = JSON.stringify(parsed.result, null, 2);
144+
for (const line of pretty.split("\n")) {
145+
console.log(` ${line}`);
146+
}
147+
if (parsed.logs?.length) {
148+
console.log("\n📋 Console logs:");
149+
for (const log of parsed.logs) {
150+
console.log(` ${log}`);
151+
}
152+
}
153+
}
154+
} catch {
155+
// not JSON
156+
}
157+
console.log();
158+
},
159+
},
160+
});
161+
162+
for await (const event of s) {
163+
if (event.type === "content_delta") {
164+
process.stdout.write(event.content);
165+
}
166+
}
167+
168+
const final = await result;
169+
console.log("\n\n" + "─".repeat(60));
170+
console.log(`Turns: ${final.numTurns} | Tokens: ${final.usage.totalTokens}`);
171+
}
172+
173+
main().catch(console.error);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stratus-sdk",
3-
"version": "0.10.0",
3+
"version": "0.11.0",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

tests/core/codemode.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,4 +1623,179 @@ describe("WorkerExecutor", () => {
16231623
const result = JSON.parse(resultStr);
16241624
expect(result.result).toEqual({ temp: 72, city: "NYC" });
16251625
});
1626+
1627+
test("handles non-Error rejection (raw string throw)", async () => {
1628+
const executor = new WorkerExecutor();
1629+
const fns = {
1630+
bad: async () => {
1631+
throw "raw string rejection";
1632+
},
1633+
};
1634+
const result = await executor.execute(
1635+
"async () => { return await codemode.bad(); }",
1636+
fns,
1637+
);
1638+
expect(result.error).toBe("raw string rejection");
1639+
});
1640+
1641+
test("handles large return values across worker boundary", async () => {
1642+
const executor = new WorkerExecutor();
1643+
const result = await executor.execute(
1644+
"async () => { return Array.from({ length: 10000 }, (_, i) => i); }",
1645+
{},
1646+
);
1647+
expect(result.error).toBeUndefined();
1648+
expect((result.result as number[]).length).toBe(10000);
1649+
});
1650+
1651+
test("handles undefined return", async () => {
1652+
const executor = new WorkerExecutor();
1653+
const result = await executor.execute("async () => { /* no return */ }", {});
1654+
expect(result.error).toBeUndefined();
1655+
// undefined doesn't survive structured clone, becomes null or undefined
1656+
expect(result.result == null).toBe(true);
1657+
});
1658+
1659+
test("handles null return", async () => {
1660+
const executor = new WorkerExecutor();
1661+
const result = await executor.execute("async () => { return null; }", {});
1662+
expect(result.error).toBeUndefined();
1663+
expect(result.result).toBeNull();
1664+
});
1665+
1666+
test("handles syntax error in code", async () => {
1667+
const executor = new WorkerExecutor();
1668+
const result = await executor.execute("async () => { if ( }", {});
1669+
expect(result.error).toBeDefined();
1670+
});
1671+
1672+
test("times out on infinite loop", async () => {
1673+
const executor = new WorkerExecutor({ timeout: 300 });
1674+
const result = await executor.execute(
1675+
"async () => { while (true) {} }",
1676+
{},
1677+
);
1678+
expect(result.error).toBe("Execution timed out");
1679+
});
1680+
1681+
test("multiple sequential executions each get a fresh worker", async () => {
1682+
const executor = new WorkerExecutor();
1683+
const results: number[] = [];
1684+
for (let i = 0; i < 5; i++) {
1685+
const result = await executor.execute(`async () => { return ${i}; }`, {});
1686+
results.push(result.result as number);
1687+
}
1688+
expect(results).toEqual([0, 1, 2, 3, 4]);
1689+
});
1690+
1691+
test("handles tool that returns slowly (worker waits on host)", async () => {
1692+
const executor = new WorkerExecutor({ timeout: 10_000 });
1693+
const fns = {
1694+
slow: async () => {
1695+
await new Promise((r) => setTimeout(r, 200));
1696+
return "slow result";
1697+
},
1698+
};
1699+
const result = await executor.execute(
1700+
"async () => { return await codemode.slow(); }",
1701+
fns,
1702+
);
1703+
expect(result.result).toBe("slow result");
1704+
});
1705+
1706+
test("code without tool calls works with empty fns map", async () => {
1707+
const executor = new WorkerExecutor();
1708+
const result = await executor.execute(
1709+
"async () => { const x = [1,2,3]; return x.map(n => n * 2); }",
1710+
{},
1711+
);
1712+
expect(result.result).toEqual([2, 4, 6]);
1713+
});
1714+
1715+
test("returning nested objects survives structured clone", async () => {
1716+
const executor = new WorkerExecutor();
1717+
const result = await executor.execute(
1718+
`async () => {
1719+
return {
1720+
users: [
1721+
{ name: "Alice", scores: [10, 20] },
1722+
{ name: "Bob", scores: [30, 40] },
1723+
],
1724+
meta: { total: 2, nested: { deep: true } },
1725+
};
1726+
}`,
1727+
{},
1728+
);
1729+
expect(result.error).toBeUndefined();
1730+
const r = result.result as any;
1731+
expect(r.users).toHaveLength(2);
1732+
expect(r.users[0].scores).toEqual([10, 20]);
1733+
expect(r.meta.nested.deep).toBe(true);
1734+
});
1735+
1736+
test("tool call that returns undefined", async () => {
1737+
const executor = new WorkerExecutor();
1738+
const fns = {
1739+
noop: async () => undefined,
1740+
};
1741+
const result = await executor.execute(
1742+
"async () => { const r = await codemode.noop(); return { got: r }; }",
1743+
fns,
1744+
);
1745+
expect(result.error).toBeUndefined();
1746+
// undefined doesn't survive structured clone in postMessage
1747+
expect((result.result as any).got == null).toBe(true);
1748+
});
1749+
1750+
test("concurrent executions on same WorkerExecutor instance", async () => {
1751+
const executor = new WorkerExecutor();
1752+
const promises = Array.from({ length: 5 }, (_, i) =>
1753+
executor.execute(`async () => { return ${i} * 10; }`, {}),
1754+
);
1755+
const results = await Promise.all(promises);
1756+
expect(results.map((r) => r.result)).toEqual([0, 10, 20, 30, 40]);
1757+
expect(results.every((r) => !r.error)).toBe(true);
1758+
});
1759+
1760+
test("error in tool preserves logs captured before the error", async () => {
1761+
const executor = new WorkerExecutor();
1762+
const fns = {
1763+
explode: async () => {
1764+
throw new Error("kaboom");
1765+
},
1766+
};
1767+
const result = await executor.execute(
1768+
`async () => {
1769+
console.log("step 1");
1770+
console.log("step 2");
1771+
await codemode.explode();
1772+
console.log("step 3");
1773+
}`,
1774+
fns,
1775+
);
1776+
expect(result.error).toBe("kaboom");
1777+
expect(result.logs).toContain("step 1");
1778+
expect(result.logs).toContain("step 2");
1779+
expect(result.logs).not.toContain("step 3");
1780+
});
1781+
1782+
test("code that accesses globalThis still works in worker", async () => {
1783+
const executor = new WorkerExecutor();
1784+
const result = await executor.execute(
1785+
"async () => { return typeof globalThis === 'object'; }",
1786+
{},
1787+
);
1788+
expect(result.error).toBeUndefined();
1789+
expect(result.result).toBe(true);
1790+
});
1791+
1792+
test("handles boolean and string return types", async () => {
1793+
const executor = new WorkerExecutor();
1794+
const r1 = await executor.execute("async () => { return true; }", {});
1795+
expect(r1.result).toBe(true);
1796+
const r2 = await executor.execute("async () => { return 'hello'; }", {});
1797+
expect(r2.result).toBe("hello");
1798+
const r3 = await executor.execute("async () => { return 0; }", {});
1799+
expect(r3.result).toBe(0);
1800+
});
16261801
});

0 commit comments

Comments
 (0)