feat: add @tag model override support and remove Qdrant dependencies
- Add model-tags parser for @Tag syntax in chat messages - Support Anthropic models (Sonnet, Haiku, Opus) via @tag - Remove Qdrant vector database from infrastructure and configs - Simplify license model config to use null fallbacks - Add greeting stream after model switch via @tag - Fix protobuf field names to camelCase for v7 compatibility - Add 429 rate limit retry logic with exponential backoff - Remove RAG references from agent harness documentation
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
|
||||
|
||||
/** All platform tool names available to every subagent. */
|
||||
const ALL_PLATFORM_TOOLS = ['SymbolLookup', 'GetChartData', 'GetTicker24h', 'WebSearch', 'FetchPage', 'ArxivSearch'];
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
||||
import type { HarnessEvent, SubagentChunkEvent, SubagentThinkingEvent } from '../harness-events.js';
|
||||
@@ -13,6 +10,62 @@ import type { ToolRegistry } from '../../tools/tool-registry.js';
|
||||
import type { MCPToolInfo } from '../../tools/mcp/mcp-tool-wrapper.js';
|
||||
import { WikiLoader, type SpawnContext } from './wiki-loader.js';
|
||||
|
||||
/** All platform tool names available to every subagent. */
|
||||
const ALL_PLATFORM_TOOLS = ['SymbolLookup', 'GetChartData', 'GetTicker24h', 'WebSearch', 'FetchPage', 'ArxivSearch'];
|
||||
|
||||
/**
|
||||
* Streaming filter that strips triple-backtick fenced code blocks from text as it
|
||||
* arrives in chunks. Holds back at most 2 characters of look-ahead so normal text
|
||||
* streams through with no perceptible delay.
|
||||
*/
|
||||
class FenceFilter {
|
||||
private buf = '';
|
||||
private inFence = false;
|
||||
|
||||
write(chunk: string): string {
|
||||
this.buf += chunk;
|
||||
return this.drain(false);
|
||||
}
|
||||
|
||||
end(): string {
|
||||
return this.drain(true);
|
||||
}
|
||||
|
||||
private drain(final: boolean): string {
|
||||
let out = '';
|
||||
while (true) {
|
||||
if (!this.inFence) {
|
||||
const start = this.buf.indexOf('```');
|
||||
if (start === -1) {
|
||||
const keep = final ? this.buf.length : Math.max(0, this.buf.length - 2);
|
||||
out += this.buf.slice(0, keep);
|
||||
this.buf = this.buf.slice(keep);
|
||||
break;
|
||||
}
|
||||
out += this.buf.slice(0, start);
|
||||
const headerEnd = this.buf.indexOf('\n', start + 3);
|
||||
if (headerEnd === -1 && !final) {
|
||||
this.buf = this.buf.slice(start);
|
||||
break;
|
||||
}
|
||||
this.inFence = true;
|
||||
this.buf = headerEnd !== -1 ? this.buf.slice(headerEnd + 1) : '';
|
||||
} else {
|
||||
const end = this.buf.indexOf('```');
|
||||
if (end === -1) {
|
||||
this.buf = final ? '' : this.buf.slice(Math.max(0, this.buf.length - 2));
|
||||
break;
|
||||
}
|
||||
this.inFence = false;
|
||||
const closingEnd = this.buf.indexOf('\n', end + 3);
|
||||
this.buf = closingEnd !== -1 ? this.buf.slice(closingEnd + 1) : this.buf.slice(end + 3);
|
||||
}
|
||||
}
|
||||
// Collapse blank lines left where code blocks were removed
|
||||
return out.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
}
|
||||
|
||||
export interface SpawnInput {
|
||||
agentName: string;
|
||||
instruction: string;
|
||||
@@ -138,13 +191,15 @@ export class SpawnService {
|
||||
);
|
||||
|
||||
let finalText = '';
|
||||
const fenceFilter = new FenceFilter();
|
||||
|
||||
for await (const [mode, data] of await stream) {
|
||||
if (signal?.aborted) break;
|
||||
|
||||
if (mode === 'messages') {
|
||||
for (const chunk of SpawnService.extractStreamChunks(data, agentName)) {
|
||||
yield chunk;
|
||||
const filtered = fenceFilter.write(chunk.content);
|
||||
if (filtered) yield { ...chunk, content: filtered };
|
||||
}
|
||||
} else if (mode === 'updates') {
|
||||
if ((data as any).agent?.messages) {
|
||||
@@ -167,6 +222,9 @@ export class SpawnService {
|
||||
}
|
||||
}
|
||||
|
||||
const tail = fenceFilter.end();
|
||||
if (tail) yield { type: 'subagent_chunk', agentName, content: tail };
|
||||
|
||||
this.logger.info(
|
||||
{ agentName, textLength: finalText.length, imageCount: imageCapture.length },
|
||||
'SpawnService: finished'
|
||||
@@ -182,12 +240,16 @@ export class SpawnService {
|
||||
|
||||
/**
|
||||
* Extract subagent_chunk / subagent_thinking events from a LangGraph `messages` stream datum.
|
||||
* Only processes AIMessageChunks — ToolMessages (identified by tool_call_id) are skipped
|
||||
* because their content is raw tool result data, not agent narrative text.
|
||||
*/
|
||||
static extractStreamChunks(
|
||||
data: unknown,
|
||||
agentName: string,
|
||||
): Array<SubagentChunkEvent | SubagentThinkingEvent> {
|
||||
const msg = Array.isArray(data) ? (data as unknown[])[0] : data;
|
||||
// ToolMessages have tool_call_id; AIMessageChunks don't — skip tool results
|
||||
if ((msg as any)?.tool_call_id != null) return [];
|
||||
const content = (msg as any)?.content;
|
||||
if (typeof content === 'string') {
|
||||
return content ? [{ type: 'subagent_chunk', agentName, content }] : [];
|
||||
|
||||
Reference in New Issue
Block a user