Skip to content

Commit 33e19ef

Browse files
committed
fix(types): resolve all TypeScript compilation errors across codebase
- Add ES2021 lib to tsconfig for AggregateError support - Fix AgentStreamResult cast-through-unknown in agency tests - Fix vi.fn mock typing for performOCR, RaptorTree, RetrievalAugmentor - Fix spawnSync mock type in provider-defaults test - Add DOM lib reference for browser-only AudioProcessor/EnvironmentalCalibrator - Cast response.json() in RedditChannelAdapter, MultiRegistryLoader, CohereReranker - Add missing hydeSearch/decomposeQuery methods to QueryRouter - Fix RetrieveGraphEvent shape in QueryDispatcher (seedCount -> entityCount) - Align PineconeVectorStore/PostgresVectorStore tests with IVectorStore interface - Fix Dirent mock cast in QueryRouter test - Fix MultimodalSearchResult import in UnifiedRetriever - Remove invalid maxDepth from GraphRAGSearchOptions usage - Replace BodyInit with any cast in speech STT providers - Fix MessageContent toLowerCase in ClaudeCodeProvider integration test - Replace RequestInfo with string|URL in test fetch mocks
1 parent 0055bd6 commit 33e19ef

24 files changed

Lines changed: 576 additions & 59 deletions

src/api/__tests__/agency-integration.test.ts

Lines changed: 256 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ beforeEach(() => {
129129
fullStream: (async function* () { yield { type: 'text', text: DEFAULT_RESULT.text }; })(),
130130
text: Promise.resolve(DEFAULT_RESULT.text),
131131
usage: Promise.resolve(DEFAULT_RESULT.usage),
132+
agentCalls: Promise.resolve(DEFAULT_RESULT.agentCalls),
132133
});
133134
});
134135

@@ -597,7 +598,7 @@ describe('Agency Full Integration', () => {
597598
// stream()
598599
// ---------------------------------------------------------------------------
599600

600-
it('stream() delegates to the compiled strategy', () => {
601+
it('stream() delegates to the compiled strategy', async () => {
601602
const team = agency({
602603
agents: { a: mockAgentConfig('a') },
603604
strategy: 'sequential',
@@ -608,11 +609,35 @@ describe('Agency Full Integration', () => {
608609
textStream: AsyncIterable<string>;
609610
};
610611

612+
await streamResult.text;
613+
611614
// The mock strategy stream is invoked and returns a valid object.
612615
expect(streamResult).toBeDefined();
613616
expect(hoisted.strategyStream).toHaveBeenCalledWith('stream test', undefined);
614617
});
615618

619+
it('stream() appends the structured-output schema hint before streaming', async () => {
620+
hoisted.strategyStream.mockClear();
621+
622+
const schema = {
623+
parse: (v: unknown) => v,
624+
shape: { score: {} },
625+
};
626+
627+
const team = agency({
628+
agents: { a: mockAgentConfig('a') },
629+
strategy: 'sequential',
630+
output: schema,
631+
});
632+
633+
const streamResult = team.stream('Return JSON') as { text: Promise<string> };
634+
await streamResult.text;
635+
636+
const calledPrompt = hoisted.strategyStream.mock.calls[0][0] as string;
637+
expect(calledPrompt).toContain('Respond with valid JSON');
638+
expect(calledPrompt).toContain('score');
639+
});
640+
616641
it('stream textStream is iterable', async () => {
617642
const team = agency({
618643
agents: { a: mockAgentConfig('a') },
@@ -630,6 +655,68 @@ describe('Agency Full Integration', () => {
630655
expect(chunks[0]).toBe(DEFAULT_RESULT.text);
631656
});
632657

658+
it('stream() applies beforeReturn HITL modifications to the resolved text', async () => {
659+
const approvalRequested = vi.fn();
660+
const approvalDecided = vi.fn();
661+
const agentEnd = vi.fn();
662+
663+
const team = agency({
664+
agents: { a: mockAgentConfig('a') },
665+
strategy: 'sequential',
666+
hitl: {
667+
approvals: { beforeReturn: true },
668+
handler: async () => ({
669+
approved: true,
670+
modifications: { output: 'approved stream output' },
671+
}),
672+
},
673+
on: { approvalRequested, approvalDecided, agentEnd },
674+
});
675+
676+
const streamResult = team.stream('stream approval') as { text: Promise<string> };
677+
678+
await expect(streamResult.text).resolves.toBe('approved stream output');
679+
expect(approvalRequested).toHaveBeenCalledOnce();
680+
expect(approvalDecided).toHaveBeenCalledOnce();
681+
expect(agentEnd).toHaveBeenCalledWith(
682+
expect.objectContaining({ output: 'approved stream output' }),
683+
);
684+
});
685+
686+
it('stream() exposes parsed output and aggregates usage once', async () => {
687+
const jsonResult = '{"score":42}';
688+
hoisted.strategyStream.mockReturnValueOnce({
689+
textStream: (async function* () { yield jsonResult; })(),
690+
fullStream: (async function* () { yield { type: 'text', text: jsonResult }; })(),
691+
text: Promise.resolve(jsonResult),
692+
usage: Promise.resolve(DEFAULT_USAGE),
693+
});
694+
695+
const schema = {
696+
parse: (v: unknown) => v,
697+
shape: { score: {} },
698+
};
699+
700+
const team = agency({
701+
agents: { a: mockAgentConfig('a') },
702+
strategy: 'sequential',
703+
output: schema,
704+
});
705+
706+
const streamResult = team.stream('stream structured') as unknown as {
707+
text: Promise<string>;
708+
usage: Promise<Record<string, unknown>>;
709+
parsed: Promise<unknown>;
710+
};
711+
712+
await expect(streamResult.text).resolves.toBe(jsonResult);
713+
await expect(streamResult.parsed).resolves.toEqual({ score: 42 });
714+
await expect(streamResult.usage).resolves.toEqual(DEFAULT_USAGE);
715+
716+
const usage = await team.usage() as Record<string, unknown>;
717+
expect(usage.totalTokens).toBe(DEFAULT_USAGE.totalTokens);
718+
});
719+
633720
// ---------------------------------------------------------------------------
634721
// Validation edge cases
635722
// ---------------------------------------------------------------------------
@@ -1180,6 +1267,48 @@ describe('Agency Full Integration', () => {
11801267
expect(chunks.join('')).toBe(DEFAULT_RESULT.text);
11811268
});
11821269

1270+
it('stream() yields text chunks incrementally before completion', async () => {
1271+
let releaseSecondChunk!: () => void;
1272+
const secondChunkGate = new Promise<void>((resolve) => {
1273+
releaseSecondChunk = resolve;
1274+
});
1275+
1276+
hoisted.strategyStream.mockReturnValueOnce({
1277+
textStream: (async function* () {
1278+
yield 'first ';
1279+
await secondChunkGate;
1280+
yield 'second';
1281+
})(),
1282+
fullStream: (async function* () {
1283+
yield { type: 'text', text: 'first ' };
1284+
await secondChunkGate;
1285+
yield { type: 'text', text: 'second' };
1286+
})(),
1287+
text: (async () => {
1288+
await secondChunkGate;
1289+
return 'first second';
1290+
})(),
1291+
usage: Promise.resolve(DEFAULT_USAGE),
1292+
agentCalls: Promise.resolve(DEFAULT_RESULT.agentCalls),
1293+
});
1294+
1295+
const team = agency({
1296+
agents: { worker: mockAgentConfig('worker') },
1297+
strategy: 'sequential',
1298+
});
1299+
1300+
const streamResult = team.stream('incremental stream') as {
1301+
textStream: AsyncIterable<string>;
1302+
text: Promise<string>;
1303+
};
1304+
const iterator = streamResult.textStream[Symbol.asyncIterator]();
1305+
1306+
await expect(iterator.next()).resolves.toEqual({ value: 'first ', done: false });
1307+
releaseSecondChunk();
1308+
await expect(iterator.next()).resolves.toEqual({ value: 'second', done: false });
1309+
await expect(streamResult.text).resolves.toBe('first second');
1310+
});
1311+
11831312
it('stream() text promise resolves to the full text', async () => {
11841313
const team = agency({
11851314
agents: { worker: mockAgentConfig('worker') },
@@ -1190,6 +1319,94 @@ describe('Agency Full Integration', () => {
11901319
const text = await streamResult.text;
11911320
expect(text).toBe(DEFAULT_RESULT.text);
11921321
});
1322+
1323+
it('stream() exposes the finalized agentCalls ledger', async () => {
1324+
const team = agency({
1325+
agents: { worker: mockAgentConfig('worker') },
1326+
strategy: 'sequential',
1327+
});
1328+
1329+
const streamResult = team.stream('stream ledger') as unknown as {
1330+
agentCalls: Promise<unknown>;
1331+
};
1332+
1333+
await expect(streamResult.agentCalls).resolves.toEqual(DEFAULT_RESULT.agentCalls);
1334+
});
1335+
1336+
it('stream() fullStream includes approval events and the finalized output event', async () => {
1337+
const team = agency({
1338+
agents: { worker: mockAgentConfig('worker') },
1339+
strategy: 'sequential',
1340+
hitl: {
1341+
approvals: { beforeReturn: true },
1342+
handler: async () => ({
1343+
approved: true,
1344+
modifications: { output: 'approved stream output' },
1345+
}),
1346+
},
1347+
});
1348+
1349+
const streamResult = team.stream('stream approval events') as {
1350+
fullStream: AsyncIterable<Record<string, unknown>>;
1351+
text: Promise<string>;
1352+
};
1353+
1354+
const parts: Array<Record<string, unknown>> = [];
1355+
for await (const part of streamResult.fullStream) {
1356+
parts.push(part);
1357+
}
1358+
1359+
await expect(streamResult.text).resolves.toBe('approved stream output');
1360+
expect(parts).toEqual(
1361+
expect.arrayContaining([
1362+
expect.objectContaining({ type: 'approval-requested' }),
1363+
expect.objectContaining({ type: 'approval-decided', approved: true }),
1364+
expect.objectContaining({
1365+
type: 'final-output',
1366+
text: 'approved stream output',
1367+
usage: DEFAULT_USAGE,
1368+
agentCalls: DEFAULT_RESULT.agentCalls,
1369+
}),
1370+
expect.objectContaining({
1371+
type: 'agent-end',
1372+
agent: '__agency__',
1373+
output: 'approved stream output',
1374+
}),
1375+
]),
1376+
);
1377+
});
1378+
1379+
it('stream() exposes finalTextStream with only the finalized approved text', async () => {
1380+
const team = agency({
1381+
agents: { worker: mockAgentConfig('worker') },
1382+
strategy: 'sequential',
1383+
hitl: {
1384+
approvals: { beforeReturn: true },
1385+
handler: async () => ({
1386+
approved: true,
1387+
modifications: { output: 'approved stream output' },
1388+
}),
1389+
},
1390+
});
1391+
1392+
const streamResult = team.stream('stream finalized text') as unknown as {
1393+
textStream: AsyncIterable<string>;
1394+
finalTextStream: AsyncIterable<string>;
1395+
};
1396+
1397+
const rawChunks: string[] = [];
1398+
for await (const chunk of streamResult.textStream) {
1399+
rawChunks.push(chunk);
1400+
}
1401+
1402+
const finalizedChunks: string[] = [];
1403+
for await (const chunk of streamResult.finalTextStream) {
1404+
finalizedChunks.push(chunk);
1405+
}
1406+
1407+
expect(rawChunks.join('')).toBe(DEFAULT_RESULT.text);
1408+
expect(finalizedChunks).toEqual(['approved stream output']);
1409+
});
11931410
});
11941411

11951412
// ---------------------------------------------------------------------------
@@ -1214,7 +1431,8 @@ describe('Agency Full Integration', () => {
12141431
await session.send('First turn');
12151432

12161433
// Second turn via stream() — should include the prior history in the prompt.
1217-
session.stream('Second turn via stream');
1434+
const result = session.stream('Second turn via stream') as { text: Promise<string> };
1435+
await result.text;
12181436

12191437
// The strategy stream should have been called with the history-prefixed prompt.
12201438
expect(hoisted.strategyStream).toHaveBeenCalledWith(
@@ -1242,20 +1460,53 @@ describe('Agency Full Integration', () => {
12421460
expect(chunks.join('')).toBe(DEFAULT_RESULT.text);
12431461
});
12441462

1245-
it('session.stream() does not lose history on first call with empty history', () => {
1463+
it('session.stream() stores the finalized assistant text and session usage', async () => {
1464+
const team = agency({
1465+
agents: { a: mockAgentConfig('a') },
1466+
strategy: 'sequential',
1467+
hitl: {
1468+
approvals: { beforeReturn: true },
1469+
handler: async () => ({
1470+
approved: true,
1471+
modifications: { output: 'approved session reply' },
1472+
}),
1473+
},
1474+
});
1475+
1476+
const session = team.session('stream-finalized-history') as {
1477+
stream: (t: string) => { text: Promise<string> };
1478+
messages: () => Array<{ role: 'user' | 'assistant'; content: string }>;
1479+
usage: () => Promise<Record<string, unknown>>;
1480+
};
1481+
1482+
const result = session.stream('Hello with approvals');
1483+
await expect(result.text).resolves.toBe('approved session reply');
1484+
await Promise.resolve();
1485+
1486+
expect(session.messages()).toEqual([
1487+
{ role: 'user', content: 'Hello with approvals' },
1488+
{ role: 'assistant', content: 'approved session reply' },
1489+
]);
1490+
1491+
const usage = await session.usage();
1492+
expect(usage.totalTokens).toBe(DEFAULT_USAGE.totalTokens);
1493+
});
1494+
1495+
it('session.stream() does not lose history on first call with empty history', async () => {
12461496
const team = agency({
12471497
agents: { a: mockAgentConfig('a') },
12481498
strategy: 'sequential',
12491499
});
12501500

12511501
const session = team.session('stream-empty-history') as {
1252-
stream: (t: string) => unknown;
1502+
stream: (t: string) => { text: Promise<string> };
12531503
};
12541504

12551505
hoisted.strategyStream.mockClear();
12561506

12571507
// When history is empty, the plain text should be passed through unchanged.
1258-
session.stream('Plain prompt');
1508+
const result = session.stream('Plain prompt');
1509+
await result.text;
12591510

12601511
expect(hoisted.strategyStream).toHaveBeenCalledWith('Plain prompt', undefined);
12611512
});

0 commit comments

Comments
 (0)