8000 fix(agent): stop capping NPC agent output with maxTokens by Mnehmos · Pull Request #111 · Mnehmos/mnehmos.rpg.mcp · GitHub
[go: up one dir, main page]

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
31 changes: 25 additions & 6 deletions src/agent/runtime/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,34 @@ export async function invokeAgent(input: InvokeInput, deps: AgentRuntimeDeps): P
deps
);

// No per-call output cap: an agent answers at whatever length it needs.
// Only an explicit per-agent budgetTokens (null by default) bounds a call,
// and the prompt is charged against it too, so reserve the prompt first.
const completionBudget = agent.budgetTokens === null
? undefined
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
: agent.budgetTokens - agent.tokensUsed - estimateTokens(composed.messages);
if (completionBudget !== undefined && completionBudget <= 0) {
const reason = `budget_exhausted (remaining ${agent.budgetTokens! - agent.tokensUsed} tokens cannot cover the prompt)`;
const call = deps.agentRepo.recordCall({
agentId: agent.id,
requestId: input.requestId ?? null,
provider: agent.provider,
model: resolvedModel,
messagesJson: '[]',
status: 'budget_exhausted',
reasoningEffort: competency?.reasoningEffort ?? null,
competencySource: competency?.source ?? null,
errorMessage: reason
});
return {
...emptyResult(agent, character, 'budget_exhausted', reason),
callId: call.id
};
}

// 5. Call provider with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), agent.timeoutMs);
const remainingBudget = agent.budgetTokens === null
? null
: Math.max(0, agent.budgetTokens - agent.tokensUsed);
const completionBudget = remainingBudget === null
? agent.maxTokens
: Math.min(agent.maxTokens, remainingBudget);

try {
const result = await provider.call({
Expand Down
53 changes: 53 additions & 0 deletions tests/agent/runtime/invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,59 @@ describe('invokeAgent', () => {
expect(result.callId).toBeTruthy();
});

it('sends no output cap when the agent has no explicit budget', async () => {
const agent = setupAgent();
expect(agent.maxTokens).toBe(800);
let sentMaxTokens: unknown = 'not-called';
factory.register('openai', {
name: 'openai',
call: async (opts) => {
sentMaxTokens = opts.maxTokens;
return { text: 'Kara waits.', promptTokens: 10, completionTokens: 5, raw: '{}', durationMs: 5, finishReason: 'stop' };
}
});

await invokeAgent({ agentId: agent.id, situation: 'Your move.' }, deps);

expect(sentMaxTokens).toBeUndefined();
});

it('bounds a call only by the remaining explicit budget', async () => {
const agent = setupAgent({ budgetTokens: 5_000 });
let sentMaxTokens: unknown = 'not-called';
factory.register('openai', {
name: 'openai',
call: async (opts) => {
sentMaxTokens = opts.maxTokens;
return { text: 'Kara waits.', promptTokens: 10, completionTokens: 5, raw: '{}', durationMs: 5, finishReason: 'stop' };
}
});

await invokeAgent({ agentId: agent.id, situation: 'Your move.' }, deps);

// The prompt is charged against the budget too, so it is reserved first.
expect(typeof sentMaxTokens).toBe('number');
expect(sentMaxTokens as number).toBeLessThan(5_000);
expect(sentMaxTokens as number).toBeGreaterThan(0);
});

it('does not call the provider when the budget cannot cover the prompt', async () => {
const agent = setupAgent({ budgetTokens: 5 });
let called = false;
factory.register('openai', {
name: 'openai',
call: async () => {
called = true;
return { text: 'x', promptTokens: 1, completionTokens: 1, raw: '{}', durationMs: 1, finishReason: 'stop' };
}
});

const result = await invokeAgent({ agentId: agent.id, situation: 'Your move.' }, deps);

expect(called).toBe(false);
expect(result.status).toBe('budget_exhausted');
});

it('derives provider model and reasoning effort from character INT', async () => {
const agent = setupAgent({ characterInt: 18 });
let observedModel = '';
Expand Down
Loading
0