正文与多工具调用的会话传输
策略的具体实现如下。
ts
export function createSessionModelTransport<const Id extends string>(
registration: SessionModelTransportRegistration & { readonly id: Id },
): ModelTransportDefinition<ResolvedSessionResult> & { readonly id: Id } {
const id = registration.id;
if (typeof id !== 'string' || !id.trim())
throw new TypeError('Session transport requires an ID');
const source = registration.tools;
const options = snapshotModelOptions(registration.options ?? {});
return Object.freeze({
id,
prepare() {
return prepareSessionFunctions([...source.values()], {
...(options.temperature === undefined ? {} : { temperature: options.temperature }),
...(options.toolChoice === undefined ? {} : { toolChoice: options.toolChoice }),
...(options.parallelToolCalls === undefined
? {}
: { parallelToolCalls: options.parallelToolCalls }),
});
},
});
}
export function prepareSessionFunctions(
tools: readonly RegisteredModelTool[],
options: SessionFunctionOptions = {},
): PreparedSessionTransport {
if (!tools.length) throw new TypeError('Session transport requires registered tools');
const names = new Map(tools.map((tool) => [wireName(tool.id), tool.id]));
if (names.size !== tools.length) throw new TypeError('Model wire name collision');
const functions = tools.map((tool) => ({
name: wireName(tool.id),
description: tool.description,
parameters: tool.inputSchema.jsonSchema,
strict: false,
}));
const instruction =
'You may answer directly or call one or more registered tools. Prose and calls may appear together. A call is a proposal, not proof of execution.';
return {
options: {
json: false,
functions,
toolChoice: options.toolChoice ?? 'auto',
...(options.parallelToolCalls === undefined
? {}
: { parallelToolCalls: options.parallelToolCalls }),
...(options.temperature === undefined ? {} : { temperature: options.temperature }),
},
schemaChars: JSON.stringify(functions).length + instruction.length,
instruction,
createResponse(): ModelResponseCollector<ResolvedSessionResult> {
const collector = createToolCallCollector();
let text = '';
return {
get outputChars() {
return (
text.length +
collector.calls.reduce(
(sum, call) => sum + call.id.length + call.name.length + call.arguments.length,
0,
)
);
},
get calls() {
try {
return collector.complete().map((call) => ({
id: call.id,
name: call.name,
argumentChars: call.arguments.length,
toolId: names.get(call.name),
}));
} catch {
return undefined;
}
},
push(part: ModelPart) {
if (typeof part === 'string') {
text += part;
return;
}
collector.push(part);
},
finish() {
const completed = collector.complete();
if (text.trim() === '' && completed.length === 0)
modelProtocolError('Model response is empty', 'MODEL_INCOMPLETE');
const calls = completed.map((call): ResolvedSessionCall => {
const toolId = names.get(call.name);
const input = parseArguments(call.arguments);
return {
index: call.index,
id: call.id,
name: call.name,
arguments: call.arguments,
argumentChars: call.arguments.length,
...(toolId === undefined ? {} : { toolId }),
...(input === undefined ? {} : { input }),
...(toolId === undefined
? { problem: 'unknown-tool' as const }
: input === undefined
? { problem: 'invalid-arguments' as const }
: {}),
};
});
return { text, calls };
},
};
},
};
}