mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Merge pull request #177 from Lint111/agent/split-transcript-tool-results
fix(transcripts): complete tools from user results
This commit is contained in:
+33
-16
@@ -40,6 +40,7 @@ interface TranscriptContentBlock {
|
||||
text?: string;
|
||||
name?: string;
|
||||
input?: Record<string, unknown>;
|
||||
tool_use_id?: string;
|
||||
content?: string;
|
||||
is_error?: boolean;
|
||||
}
|
||||
@@ -328,10 +329,7 @@ export class TranscriptWatcher extends EventEmitter {
|
||||
this.handleResultEntry(entry);
|
||||
break;
|
||||
case 'user':
|
||||
// User message means new turn, reset some state
|
||||
this.state.isComplete = false;
|
||||
this.state.hasError = false;
|
||||
this.state.errorMessage = null;
|
||||
this.handleUserEntry(entry);
|
||||
break;
|
||||
case 'system':
|
||||
// System messages are informational
|
||||
@@ -360,23 +358,42 @@ export class TranscriptWatcher extends EventEmitter {
|
||||
this.state.currentTool = block.name;
|
||||
this.emit('transcript:tool_start', block.name);
|
||||
} else if (block.type === 'tool_result') {
|
||||
// Tool completed
|
||||
const wasError = block.is_error === true;
|
||||
const toolName = this.state.currentTool;
|
||||
this.state.toolExecuting = false;
|
||||
this.state.currentTool = null;
|
||||
if (toolName) {
|
||||
this.emit('transcript:tool_end', toolName, wasError);
|
||||
}
|
||||
if (wasError && block.content) {
|
||||
this.state.hasError = true;
|
||||
this.state.errorMessage = String(block.content).slice(0, 200);
|
||||
}
|
||||
this.handleToolResult(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleUserEntry(entry: TranscriptEntry): void {
|
||||
// A user-authored prompt starts a turn, while Claude tool results also use
|
||||
// user entries. Reset turn state first, then close any completed tool.
|
||||
this.state.isComplete = false;
|
||||
this.state.hasError = false;
|
||||
this.state.errorMessage = null;
|
||||
|
||||
const content = entry.message?.content;
|
||||
if (!Array.isArray(content)) return;
|
||||
for (const block of content) {
|
||||
if (block.type === 'tool_result') {
|
||||
this.handleToolResult(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleToolResult(block: TranscriptContentBlock): void {
|
||||
const wasError = block.is_error === true;
|
||||
const toolName = this.state.currentTool;
|
||||
this.state.toolExecuting = false;
|
||||
this.state.currentTool = null;
|
||||
if (toolName) {
|
||||
this.emit('transcript:tool_end', toolName, wasError);
|
||||
}
|
||||
if (wasError && block.content) {
|
||||
this.state.hasError = true;
|
||||
this.state.errorMessage = String(block.content).slice(0, 200);
|
||||
}
|
||||
}
|
||||
|
||||
private handleResultEntry(entry: TranscriptEntry): void {
|
||||
// Result entry indicates completion
|
||||
this.state.isComplete = true;
|
||||
|
||||
@@ -88,14 +88,16 @@ describe('TranscriptWatcher', () => {
|
||||
watcher.start(testFile);
|
||||
|
||||
// Add user entry
|
||||
const userEntry = { type: 'user', timestamp: new Date().toISOString(), message: { role: 'user', content: 'test' } };
|
||||
const userEntry = {
|
||||
type: 'user',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: { role: 'user', content: 'test' },
|
||||
};
|
||||
appendFileSync(testFile, JSON.stringify(userEntry) + '\n');
|
||||
|
||||
// Wait for processing
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const state = watcher.getState();
|
||||
expect(state.entryCount).toBeGreaterThanOrEqual(1);
|
||||
await vi.waitFor(() => {
|
||||
expect(watcher.getState().entryCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit transcript:complete on result entry', async () => {
|
||||
@@ -109,10 +111,9 @@ describe('TranscriptWatcher', () => {
|
||||
const resultEntry = { type: 'result', timestamp: new Date().toISOString() };
|
||||
appendFileSync(testFile, JSON.stringify(resultEntry) + '\n');
|
||||
|
||||
// Wait for processing
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(completeHandler).toHaveBeenCalled();
|
||||
await vi.waitFor(() => {
|
||||
expect(completeHandler).toHaveBeenCalled();
|
||||
});
|
||||
const state = watcher.getState();
|
||||
expect(state.isComplete).toBe(true);
|
||||
});
|
||||
@@ -130,22 +131,62 @@ describe('TranscriptWatcher', () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'Read', input: { file_path: '/test.txt' } }
|
||||
]
|
||||
}
|
||||
content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/test.txt' } }],
|
||||
},
|
||||
};
|
||||
appendFileSync(testFile, JSON.stringify(assistantEntry) + '\n');
|
||||
|
||||
// Wait for processing
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(toolStartHandler).toHaveBeenCalledWith('Read');
|
||||
await vi.waitFor(() => {
|
||||
expect(toolStartHandler).toHaveBeenCalledWith('Read');
|
||||
});
|
||||
const state = watcher.getState();
|
||||
expect(state.toolExecuting).toBe(true);
|
||||
expect(state.currentTool).toBe('Read');
|
||||
});
|
||||
|
||||
it('should complete a tool when Claude writes tool_result in a user entry', async () => {
|
||||
writeFileSync(testFile, '');
|
||||
watcher.start(testFile);
|
||||
|
||||
const toolEndHandler = vi.fn();
|
||||
watcher.on('transcript:tool_end', toolEndHandler);
|
||||
|
||||
appendFileSync(
|
||||
testFile,
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'tool_use', name: 'Bash', input: { command: 'printf done' } }],
|
||||
},
|
||||
}) + '\n'
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
expect(watcher.getState().toolExecuting).toBe(true);
|
||||
});
|
||||
|
||||
appendFileSync(
|
||||
testFile,
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'done', is_error: false }],
|
||||
},
|
||||
}) + '\n'
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(toolEndHandler).toHaveBeenCalledWith('Bash', false);
|
||||
});
|
||||
expect(watcher.getState()).toMatchObject({
|
||||
toolExecuting: false,
|
||||
currentTool: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect plan mode from AskUserQuestion tool', async () => {
|
||||
writeFileSync(testFile, '');
|
||||
watcher.start(testFile);
|
||||
@@ -159,17 +200,14 @@ describe('TranscriptWatcher', () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'AskUserQuestion', input: { question: 'test?' } }
|
||||
]
|
||||
}
|
||||
content: [{ type: 'tool_use', name: 'AskUserQuestion', input: { question: 'test?' } }],
|
||||
},
|
||||
};
|
||||
appendFileSync(testFile, JSON.stringify(assistantEntry) + '\n');
|
||||
|
||||
// Wait for processing
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(planModeHandler).toHaveBeenCalled();
|
||||
await vi.waitFor(() => {
|
||||
expect(planModeHandler).toHaveBeenCalled();
|
||||
});
|
||||
const state = watcher.getState();
|
||||
expect(state.planModeDetected).toBe(true);
|
||||
});
|
||||
@@ -182,15 +220,14 @@ describe('TranscriptWatcher', () => {
|
||||
const resultEntry = {
|
||||
type: 'result',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: { type: 'api_error', message: 'Rate limited' }
|
||||
error: { type: 'api_error', message: 'Rate limited' },
|
||||
};
|
||||
appendFileSync(testFile, JSON.stringify(resultEntry) + '\n');
|
||||
|
||||
// Wait for processing
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(watcher.getState().hasError).toBe(true);
|
||||
});
|
||||
const state = watcher.getState();
|
||||
expect(state.hasError).toBe(true);
|
||||
expect(state.errorMessage).toContain('Rate limited');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user