Skip to content
Merged
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
1 change: 1 addition & 0 deletions .bun-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.4.4
9 changes: 8 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,15 @@ jobs:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- uses: oven-sh/setup-bun@v2
with:
bun-version-file: .bun-version

- name: Test typecheck runner
run: npm run test:typecheck

- name: TypeScript type-check
run: npx tsc --noEmit
run: npm run typecheck

circular-deps:
needs: install
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@
"prepublishOnly": "npm run build",
"build": "tsdown && tsc -p tsconfig.build.json",
"build:dev": "tsdown",
"typecheck": "node scripts/typecheck.ts",
"test:typecheck": "node --test scripts/typecheck.test.ts",
"check:circular-deps": "node config/circular-deps.mjs",
"test:circular-deps": "node --test config/circular-deps.test.mjs",
"test:light-entries": "node --test config/light-entries.test.mjs",
Expand Down
121 changes: 121 additions & 0 deletions scripts/typecheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { test } from 'node:test';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import type { TestContext } from 'node:test';

const runner = fileURLToPath(new URL('./typecheck.ts', import.meta.url));

async function fixture(t: TestContext, bun?: string): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'agents-typecheck-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(join(root, 'bin'));
await writeFile(
join(root, 'tsconfig.json'),
JSON.stringify({
compilerOptions: { strict: true, types: [], skipLibCheck: true },
})
);
await writeFile(
join(root, 'index.ts'),
'export const value: string = "ok";\n'
);
if (bun !== undefined) {
await writeFile(join(root, 'bin', 'bun'), `#!${process.execPath}\n${bun}`, {
mode: 0o755,
});
}
return root;
}

function run(
root: string,
args: string[] = [],
script = runner
): ReturnType<typeof spawnSync> {
return spawnSync(process.execPath, [script, ...args], {
cwd: root,
env: { ...process.env, PATH: join(root, 'bin') },
encoding: 'utf8',
});
}

test('prefers bun check and forwards project arguments', async (t) => {
const root = await fixture(
t,
`if (process.argv.includes('--help')) process.exit(0);
console.log(JSON.stringify(process.argv.slice(2)));`
);
const result = run(root, ['-p', 'tsconfig.json']);
assert.equal(result.status, 0, String(result.stderr));
assert.deepEqual(JSON.parse(String(result.stdout)), [
'check',
'-p',
'tsconfig.json',
]);
assert.equal(result.stderr, '');
});

test('preserves Bun diagnostics and failure without falling back', async (t) => {
const root = await fixture(
t,
`if (process.argv.includes('--help')) process.exit(0);
console.error('fixture type error');
process.exit(42);`
);
const result = run(root);
assert.equal(result.status, 42);
assert.equal(result.stderr, 'fixture type error\n');
});

test('fails when the selected checker is interrupted', async (t) => {
const root = await fixture(
t,
`if (process.argv.includes('--help')) process.exit(0);
process.kill(process.pid, 'SIGTERM');`
);
assert.equal(run(root).status, 1);
});

for (const [name, bun] of [
['missing Bun', undefined],
['Bun without check', 'process.exit(1);'],
] as const) {
test(`falls back to local tsc with ${name} without emitting`, async (t) => {
const root = await fixture(t, bun);
const result = run(root, ['-p', 'tsconfig.json', '--listEmittedFiles']);
assert.equal(
result.status,
0,
String(result.stdout) + String(result.stderr)
);
assert.match(String(result.stderr), /falling back to tsc/);
assert.equal(result.stdout, '');
});

test(`preserves tsc diagnostics and failure with ${name}`, async (t) => {
const root = await fixture(t, bun);
await writeFile(
join(root, 'index.ts'),
'export const value: string = 123;\n'
);
const result = run(root);
assert.equal(result.status, 2);
assert.match(String(result.stdout), /TS2322/);
});
}

test('explains how to recover when neither checker is available', async (t) => {
const root = await fixture(t);
const script = join(root, 'typecheck.ts');
await copyFile(runner, script);
const result = run(root, [], script);
assert.equal(result.status, 1);
assert.match(
String(result.stderr),
/neither `bun check` nor typescript is available; run npm ci/
);
});
31 changes: 31 additions & 0 deletions scripts/typecheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createRequire } from 'node:module';
import { spawnSync } from 'node:child_process';

const hasBunCheck =
spawnSync('bun', ['check', '--help'], { stdio: 'ignore' }).status === 0;
let command = 'bun';
let args = ['check'];

if (!hasBunCheck) {
try {
const tsc = createRequire(import.meta.url).resolve('typescript/bin/tsc');
command = process.execPath;
args = [tsc, '--noEmit'];
} catch {
console.error(
'typecheck: neither `bun check` nor typescript is available; run npm ci'
);
process.exit(1);
}
console.error(
'typecheck: `bun check` is unavailable, falling back to tsc; install Bun 1.4.4 for faster checks'
);
}

const result = spawnSync(command, [...args, ...process.argv.slice(2)], {
stdio: 'inherit',
});
if (result.error) {
console.error(`typecheck: ${result.error.message}`);
}
process.exit(result.status ?? 1);
6 changes: 2 additions & 4 deletions src/llm/openai/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ function _convertReasoningSummaryToOpenAIResponsesParams(
reasoning: ChatOpenAIReasoningSummary
): OpenAIClient.Responses.ResponseReasoningItem {
// combine summary parts that have the the same index and then remove the indexes
const summary = (
const summary: OpenAIClient.Responses.ResponseReasoningItem.Summary[] = (
reasoning.summary.length > 1
? reasoning.summary.reduce(
(acc, curr) => {
Expand All @@ -576,9 +576,7 @@ function _convertReasoningSummaryToOpenAIResponsesParams(
[{ ...reasoning.summary[0] }]
)
: reasoning.summary
).map((s) =>
Object.fromEntries(Object.entries(s).filter(([k]) => k !== 'index'))
) as OpenAIClient.Responses.ResponseReasoningItem.Summary[];
).map(({ index: _index, ...part }) => part);

return {
...reasoning,
Expand Down
40 changes: 40 additions & 0 deletions src/llm/openai/utils/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ import {
} from './index';
import { calculateMaxToolCallInputChars, HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';

describe('Responses reasoning summaries', () => {
it.each([
{ name: 'empty', parts: [] as [number, string][], expected: [] },
{ name: 'single', parts: [[0, 'Thinking']], expected: ['Thinking'] },
{
name: 'streamed',
parts: [
[0, ''],
[0, 'First '],
[0, 'part'],
[1, 'Second part'],
],
expected: ['First part', 'Second part'],
},
])('strips indexes from $name summaries', ({ parts, expected }) => {
const message = new AIMessage({
content: '',
additional_kwargs: {
reasoning: {
id: 'rs_summary',
type: 'reasoning',
summary: parts.map(([index, text]) => ({
index,
text,
type: 'summary_text',
})),
},
},
});

const items = _convertMessagesToOpenAIResponsesParams([message]);

expect(items[0]).toEqual({
id: 'rs_summary',
type: 'reasoning',
summary: expected.map((text) => ({ type: 'summary_text', text })),
});
});
});

describe('_convertMessagesToOpenAIParams', () => {
it('includes reasoning_content for assistant messages in tool-call context when requested', () => {
const messages = [
Expand Down
39 changes: 39 additions & 0 deletions src/tools/__tests__/CloudflareSandboxExecution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,45 @@ describe('Cloudflare sandbox execution backend', () => {
});

describe('Cloudflare bridge runtime', () => {
it.each(['utf8', 'base64', 'stream'] as const)(
'preserves %s file contents in the HTTP body',
async (encoding) => {
const bytes = Buffer.from('héllo\0');
const fetchImpl: typeof fetch = async (input, init) => {
const request = new Request(input, init);
expect(request.method).toBe('PUT');
expect(request.headers.get('Content-Type')).toBe(
'application/octet-stream'
);
expect(Buffer.from(await request.arrayBuffer())).toEqual(bytes);
return Response.json({ ok: true });
};
const runtime = createCloudflareBridgeRuntime({
baseURL: 'https://bridge.example',
sandboxId: 'abc',
fetch: fetchImpl,
});
const content =
encoding === 'stream'
? new ReadableStream<Uint8Array>({
start(controller) {
const chunk = new Uint8Array(
new SharedArrayBuffer(bytes.length)
);
chunk.set(bytes);
controller.enqueue(chunk.subarray(0, 2));
controller.enqueue(chunk.subarray(2));
controller.close();
},
})
: bytes.toString(encoding);

await expect(
runtime.writeFile('hello.txt', content, { encoding })
).resolves.toEqual({ ok: true });
}
);

it('preserves caller-provided sandbox ids', async () => {
const urls: string[] = [];
const fetchImpl: typeof fetch = async (input) => {
Expand Down
4 changes: 2 additions & 2 deletions src/tools/cloudflare/CloudflareBridgeRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function commandWithEnv(

async function collectReadableStream(
stream: ReadableStream<Uint8Array>
): Promise<Uint8Array> {
): Promise<ReturnType<typeof Uint8Array.from>> {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
Expand All @@ -183,7 +183,7 @@ async function collectReadableStream(
async function normalizeWriteBody(
content: string | ReadableStream<Uint8Array>,
options?: { encoding?: string }
): Promise<Uint8Array> {
): Promise<ReturnType<typeof Uint8Array.from>> {
if (typeof content !== 'string') {
return collectReadableStream(content);
}
Expand Down
Loading