mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix(cli): preserve custom tool-progress payloads and honor final-only for goal summaries
- `tool_progress` JSON now carries `custom_kind`/`custom_data`, so stream-json
clients keep structured `kind: "custom"` payloads (e.g. the MCP OAuth
authorization URL) instead of an opaque `{"kind":"custom"}`.
- The headless `/goal` summary is suppressed on stdout under
`--final-message-only`, matching the single-final-assistant-message contract
(the goal status is still conveyed by the exit code).
This commit is contained in:
parent
46dc04d982
commit
ebcaa405d6
3 changed files with 62 additions and 1 deletions
|
|
@ -287,8 +287,13 @@ async function runHeadlessGoal(
|
|||
} finally {
|
||||
unsubscribeGoalEvents();
|
||||
const snapshot = completedSnapshot ?? (await session.getGoal()).goal;
|
||||
// The goal status is conveyed by the exit code; `--final-message-only` keeps
|
||||
// stdout to the single final assistant message, so the JSON summary is
|
||||
// suppressed in that mode (the text summary stays on stderr).
|
||||
if (outputFormat === 'stream-json') {
|
||||
stdout.write(`${JSON.stringify(goalSummaryJson(snapshot))}\n`);
|
||||
if (!finalOnly) {
|
||||
stdout.write(`${JSON.stringify(goalSummaryJson(snapshot))}\n`);
|
||||
}
|
||||
} else {
|
||||
stderr.write(`${formatGoalSummaryText(snapshot)}\n`);
|
||||
}
|
||||
|
|
@ -1284,6 +1289,10 @@ function toolProgressMessage(event: Extract<Event, { type: 'tool.progress' }>):
|
|||
};
|
||||
if (event.update.text !== undefined) message['text'] = event.update.text;
|
||||
if (event.update.percent !== undefined) message['percent'] = event.update.percent;
|
||||
// `kind: 'custom'` updates (e.g. the MCP OAuth authorization URL) carry their
|
||||
// payload here; keep it so stream-json clients get the structured signal.
|
||||
if (event.update.customKind !== undefined) message['custom_kind'] = event.update.customKind;
|
||||
if (event.update.customData !== undefined) message['custom_data'] = event.update.customData;
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -189,6 +189,27 @@ describe('runPrompt headless goal mode', () => {
|
|||
expect(stdout.text()).toContain('"status":"complete"');
|
||||
});
|
||||
|
||||
it('suppresses the JSON goal summary in final-message-only mode', async () => {
|
||||
mocks.session.prompt.mockImplementationOnce(async () => {
|
||||
for (const handler of mocks.eventHandlers) {
|
||||
handler(mocks.mainEvent({ type: 'turn.started', turnId: 1, origin: { kind: 'user' } }));
|
||||
handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 1, delta: 'shipped' }));
|
||||
handler(mocks.mainEvent({ type: 'turn.ended', turnId: 1, reason: 'completed' }));
|
||||
}
|
||||
});
|
||||
const stdout = writer();
|
||||
const stderr = writer();
|
||||
|
||||
await runPrompt(opts({ outputFormat: 'stream-json', finalMessageOnly: true }), 'test', {
|
||||
stdout,
|
||||
stderr,
|
||||
process: { once: () => {}, off: () => {}, exit: () => undefined as never },
|
||||
});
|
||||
|
||||
expect(stdout.text()).toBe('{"role":"assistant","content":"shipped"}\n');
|
||||
expect(stdout.text()).not.toContain('goal.summary');
|
||||
});
|
||||
|
||||
it('sets a distinct exit code for a non-complete final status', async () => {
|
||||
mocks.session.getGoal.mockResolvedValue({ goal: snapshot({ status: 'blocked' }) } as never);
|
||||
const stdout = writer();
|
||||
|
|
|
|||
|
|
@ -1523,6 +1523,37 @@ describe('runPrompt', () => {
|
|||
expect(stderr.text()).toBe('');
|
||||
});
|
||||
|
||||
it('preserves custom tool.progress payloads (e.g. MCP OAuth URL) in stream-json', async () => {
|
||||
mocks.session.prompt.mockImplementationOnce(async () => {
|
||||
for (const handler of mocks.eventHandlers) {
|
||||
handler(mocks.mainEvent({ type: 'turn.started', turnId: 75, origin: { kind: 'user' } }));
|
||||
handler(
|
||||
mocks.mainEvent({
|
||||
type: 'tool.progress',
|
||||
turnId: 75,
|
||||
toolCallId: 'tc_1',
|
||||
update: {
|
||||
kind: 'custom',
|
||||
customKind: 'mcp.oauth.authorization_url',
|
||||
customData: { serverName: 'fs', authorizationUrl: 'https://auth.example/x' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
handler(mocks.mainEvent({ type: 'turn.ended', turnId: 75, reason: 'completed' }));
|
||||
}
|
||||
});
|
||||
const stdout = writer();
|
||||
|
||||
await runPrompt(opts({ outputFormat: 'stream-json' }), '1.2.3-test', {
|
||||
stdout,
|
||||
stderr: writer(),
|
||||
});
|
||||
|
||||
expect(stdout.text()).toContain(
|
||||
'{"type":"tool_progress","tool_call_id":"tc_1","kind":"custom","custom_kind":"mcp.oauth.authorization_url","custom_data":{"serverName":"fs","authorizationUrl":"https://auth.example/x"}}',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps tool.progress on stderr (not JSON) in text mode', async () => {
|
||||
mocks.session.prompt.mockImplementationOnce(async () => {
|
||||
for (const handler of mocks.eventHandlers) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue