testronaut 1.9.3 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -3
- package/TODO.md +11 -0
- package/bin/cli.js +45 -13
- package/bin/init.js +10 -5
- package/bin/initHelpers.js +9 -3
- package/core/agent.js +75 -2
- package/core/config.js +16 -0
- package/core/launchProtocol.js +42 -0
- package/core/turnLoop.js +38 -20
- package/llm/anthropic/anthropicProvider.js +2 -0
- package/llm/gemini/geminiProvider.js +36 -8
- package/llm/openAI/openaiProvider.js +2 -0
- package/package.json +1 -1
- package/runner/testronaut.js +24 -8
- package/tests/binTests/cli.integration.test.js +26 -1
- package/tests/binTests/initHelpersTests/models.test.js +7 -1
- package/tests/coreTests/agent.test.js +61 -0
- package/tests/coreTests/config.test.js +10 -0
- package/tests/coreTests/launchProtocol.test.js +38 -0
- package/tests/coreTests/turnLoop.test.js +21 -0
- package/tests/llmTests/geminiProvider.test.js +89 -7
- package/tests/runnerTests/testronaut.test.js +33 -6
- package/tests/toolsTests/chromeBrowserProbe.test.js +38 -0
- package/tests/toolsTests/contextControl.test.js +33 -1
- package/tests/toolsTests/domControl.test.js +12 -0
- package/tests/toolsTests/toolSchema.test.js +8 -0
- package/tests/toolsTests/turnLoopUtils.test.js +16 -0
- package/tools/chromeBrowser.js +50 -5
- package/tools/contextControl.js +11 -1
- package/tools/domControl.js +20 -1
- package/tools/generateHtmlReport.js +1 -1
- package/tools/tokenControl.js +3 -3
- package/tools/toolSchema.js +4 -15
- package/tools/turnLoopUtils.js +4 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ChromeBrowser } from '../../tools/chromeBrowser.js';
|
|
3
|
+
|
|
4
|
+
function makeBrowser({ visible = true, content = 'Mission Tasks' } = {}) {
|
|
5
|
+
const locator = {
|
|
6
|
+
first: () => locator,
|
|
7
|
+
waitFor: vi.fn(() => visible ? Promise.resolve() : Promise.reject(new Error('not visible'))),
|
|
8
|
+
textContent: vi.fn().mockResolvedValue(content),
|
|
9
|
+
};
|
|
10
|
+
const browser = Object.create(ChromeBrowser.prototype);
|
|
11
|
+
browser.page = {
|
|
12
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
waitForLoadState: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
locator: vi.fn(() => locator),
|
|
15
|
+
url: vi.fn(() => 'https://example.test/dashboard'),
|
|
16
|
+
};
|
|
17
|
+
return { browser, locator };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('ChromeBrowser.probe', () => {
|
|
21
|
+
it('passes after navigating when selector and text match', async () => {
|
|
22
|
+
const { browser } = makeBrowser();
|
|
23
|
+
await expect(browser.probe({
|
|
24
|
+
url: 'https://example.test',
|
|
25
|
+
selector: '#dashboard',
|
|
26
|
+
text: 'Mission Tasks',
|
|
27
|
+
})).resolves.toMatchObject({ passed: true });
|
|
28
|
+
expect(browser.page.goto).toHaveBeenCalledWith('https://example.test', expect.any(Object));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('fails without invoking an agent when the selector is not visible', async () => {
|
|
32
|
+
const { browser } = makeBrowser({ visible: false });
|
|
33
|
+
await expect(browser.probe({ selector: '#dashboard' })).resolves.toMatchObject({
|
|
34
|
+
passed: false,
|
|
35
|
+
reason: 'selector not visible: #dashboard',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -109,6 +109,38 @@ describe('pruneConversationContext', () => {
|
|
|
109
109
|
expect(nonSystem.map(m => m.content)).toEqual(['u-2', 'a-2']);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
it('keeps the latest real user instruction as an anchor for long tool histories', () => {
|
|
113
|
+
const messages = [
|
|
114
|
+
{ role: 'system', content: 'system' },
|
|
115
|
+
{ role: 'user', content: 'Complete the mission' },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < 25; i += 1) {
|
|
119
|
+
messages.push({
|
|
120
|
+
role: 'assistant',
|
|
121
|
+
tool_calls: [{
|
|
122
|
+
id: `call-${i}`,
|
|
123
|
+
type: 'function',
|
|
124
|
+
function: { name: 'get_dom', arguments: '{}' },
|
|
125
|
+
}],
|
|
126
|
+
});
|
|
127
|
+
messages.push({
|
|
128
|
+
role: 'tool',
|
|
129
|
+
tool_call_id: `call-${i}`,
|
|
130
|
+
name: 'get_dom',
|
|
131
|
+
content: `<main>${i}</main>`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pruned = pruneConversationContext(messages, { maxNonSystemMessages: 10 });
|
|
136
|
+
|
|
137
|
+
expect(pruned[0]).toEqual({ role: 'system', content: 'system' });
|
|
138
|
+
expect(pruned[1]).toEqual({ role: 'user', content: 'Complete the mission' });
|
|
139
|
+
expect(pruned.slice(2)).toHaveLength(10);
|
|
140
|
+
expect(pruned[2].role).toBe('assistant');
|
|
141
|
+
expect(pruned[3].role).toBe('tool');
|
|
142
|
+
});
|
|
143
|
+
|
|
112
144
|
it('drops tool messages whose parent assistant tool_calls were pruned', () => {
|
|
113
145
|
const messages = [
|
|
114
146
|
// Old assistant + tool (should be pruned)
|
|
@@ -592,4 +624,4 @@ describe('summarizeGroundControlForPrompt', () => {
|
|
|
592
624
|
expect(snapshot.navigation.routeRole).toBe('dashboard');
|
|
593
625
|
expect(snapshot.navigation.currentLabel).toBe('Main dashboard');
|
|
594
626
|
});
|
|
595
|
-
});
|
|
627
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
collapseLongContent,
|
|
17
17
|
chunkBySection,
|
|
18
18
|
reduceRepeatedDivs,
|
|
19
|
+
reduceTableRows,
|
|
19
20
|
removeObfuscatedClassNames,
|
|
20
21
|
removeAutoGeneratedClasses,
|
|
21
22
|
minifyDomWhitespace,
|
|
@@ -232,6 +233,17 @@ describe('domControl helpers', () => {
|
|
|
232
233
|
});
|
|
233
234
|
});
|
|
234
235
|
|
|
236
|
+
describe('reduceTableRows', () => {
|
|
237
|
+
it('keeps headers, trims body rows, and adds a marker', () => {
|
|
238
|
+
const $ = cheerio.load('<table><thead><tr><th>Name</th></tr></thead><tbody><tr><td>A</td></tr><tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>');
|
|
239
|
+
reduceTableRows($, 2);
|
|
240
|
+
expect($('thead tr')).toHaveLength(1);
|
|
241
|
+
expect($('tbody > tr')).toHaveLength(3);
|
|
242
|
+
expect($('tbody > tr').eq(1).text()).toBe('B');
|
|
243
|
+
expect($('tbody > tr').eq(2).attr('data-collapsed')).toBe('true');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
235
247
|
describe('removeObfuscatedClassNames', () => {
|
|
236
248
|
it('removes short and hashed-looking class names', () => {
|
|
237
249
|
const html = `
|
|
@@ -2,6 +2,14 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import toolsSchema from '../../tools/toolSchema.js';
|
|
3
3
|
|
|
4
4
|
describe('tools/toolSchema', () => {
|
|
5
|
+
it('does not advertise the unimplemented get_dom_chunk tool', () => {
|
|
6
|
+
expect(toolsSchema.map(tool => tool.function.name)).not.toContain('get_dom_chunk');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('allows get_dom calls to be scoped by selector', () => {
|
|
10
|
+
const entry = toolsSchema.find(tool => tool.function.name === 'get_dom');
|
|
11
|
+
expect(entry.function.parameters.properties.selector.type).toBe('string');
|
|
12
|
+
});
|
|
5
13
|
it('includes resource_progress tool with empty params', () => {
|
|
6
14
|
const entry = toolsSchema.find(t => t.function?.name === 'resource_progress');
|
|
7
15
|
expect(entry?.type).toBe('function');
|
|
@@ -2,6 +2,22 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
import { finalResponseHandler } from '../../tools/turnLoopUtils.js';
|
|
3
3
|
|
|
4
4
|
describe('tools/turnLoopUtils', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['SUCCESS: completed', true],
|
|
7
|
+
['FAILURE: blocked', false],
|
|
8
|
+
['RESULT: SUCCESS - all checkpoints passed', true],
|
|
9
|
+
['RESULT: FAILURE - checkpoint failed', false],
|
|
10
|
+
['FINAL: success', true],
|
|
11
|
+
['**RESULT: SUCCESS**', true],
|
|
12
|
+
])('recognizes supported verdict form %s', (content, success) => {
|
|
13
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
14
|
+
try {
|
|
15
|
+
expect(finalResponseHandler({ content })).toMatchObject({ success });
|
|
16
|
+
} finally {
|
|
17
|
+
logSpy.mockRestore();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
5
21
|
it('redacts verification codes before printing and returning final responses', () => {
|
|
6
22
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
7
23
|
|
package/tools/chromeBrowser.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as cheerio from 'cheerio';
|
|
|
14
14
|
import {
|
|
15
15
|
chunkBySection,
|
|
16
16
|
reduceRepeatedDivs,
|
|
17
|
+
reduceTableRows,
|
|
17
18
|
removeAutoGeneratedClasses,
|
|
18
19
|
removeNonContentTags,
|
|
19
20
|
removeObfuscatedClassNames,
|
|
@@ -57,6 +58,7 @@ export class ChromeBrowser {
|
|
|
57
58
|
this.domListLimit = options.domListLimit;
|
|
58
59
|
this.debugEnabled = !!options.debug;
|
|
59
60
|
this._domLimitLogged = false;
|
|
61
|
+
this.storageStatePath = options.storageStatePath;
|
|
60
62
|
|
|
61
63
|
const defaultResourceGuard = {
|
|
62
64
|
enabled: true,
|
|
@@ -601,9 +603,12 @@ export class ChromeBrowser {
|
|
|
601
603
|
|
|
602
604
|
ensureDir(FILES_DIR); // ⬅️ make sure missions/files exists
|
|
603
605
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
606
|
+
const contextOptions = { acceptDownloads: true };
|
|
607
|
+
if (this.storageStatePath && fs.existsSync(this.storageStatePath)) {
|
|
608
|
+
contextOptions.storageState = this.storageStatePath;
|
|
609
|
+
this._debugLog('Restoring shared authentication state', { path: this.storageStatePath });
|
|
610
|
+
}
|
|
611
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
607
612
|
// this.context = await this.browser.newContext();
|
|
608
613
|
const first = await this.context.newPage();
|
|
609
614
|
const id = this._addAndFocus(first);
|
|
@@ -636,6 +641,26 @@ export class ChromeBrowser {
|
|
|
636
641
|
return `navigated to ${this.page.url()}`;
|
|
637
642
|
}
|
|
638
643
|
|
|
644
|
+
async probe({ url, selector, text, timeout = 5000 } = {}) {
|
|
645
|
+
if (url) {
|
|
646
|
+
await this.page.goto(url, { timeout: 30_000, waitUntil: 'domcontentloaded' });
|
|
647
|
+
await this.page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let locator = this.page.locator(selector || 'body').first();
|
|
651
|
+
if (selector) {
|
|
652
|
+
const visible = await locator.waitFor({ state: 'visible', timeout }).then(() => true).catch(() => false);
|
|
653
|
+
if (!visible) return { passed: false, reason: `selector not visible: ${selector}`, url: this.page.url() };
|
|
654
|
+
}
|
|
655
|
+
if (text) {
|
|
656
|
+
const content = await locator.textContent({ timeout }).catch(() => '');
|
|
657
|
+
if (!String(content || '').includes(String(text))) {
|
|
658
|
+
return { passed: false, reason: `text not found: ${text}`, url: this.page.url() };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { passed: true, url: this.page.url() };
|
|
662
|
+
}
|
|
663
|
+
|
|
639
664
|
async navigateEmailLink({ url, allowedHosts = [] }) {
|
|
640
665
|
const destination = new URL(url);
|
|
641
666
|
if (destination.protocol !== 'https:' || destination.username || destination.password) {
|
|
@@ -859,7 +884,7 @@ export class ChromeBrowser {
|
|
|
859
884
|
|
|
860
885
|
|
|
861
886
|
async get_dom(
|
|
862
|
-
{ limit = 100000, exclude = true, focus = [], loadState = 'networkidle', timeout = 10000 } = {},
|
|
887
|
+
{ limit = 100000, exclude = true, selector, focus = [], loadState = 'networkidle', timeout = 10000 } = {},
|
|
863
888
|
agentMemory = {}
|
|
864
889
|
) {
|
|
865
890
|
const matchesHrefPattern = (href = '') => {
|
|
@@ -985,9 +1010,21 @@ export class ChromeBrowser {
|
|
|
985
1010
|
// --- 4) Optional DOM reduction/cleanup (your existing steps) ---
|
|
986
1011
|
let $ = cheerio.load(html);
|
|
987
1012
|
|
|
1013
|
+
if (selector) {
|
|
1014
|
+
let selection;
|
|
1015
|
+
try {
|
|
1016
|
+
selection = $(selector);
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
throw new Error(`Invalid get_dom selector "${selector}": ${error.message}`);
|
|
1019
|
+
}
|
|
1020
|
+
if (!selection.length) return `[get_dom: no elements matched selector "${selector}"]`;
|
|
1021
|
+
$ = cheerio.load(selection.toString());
|
|
1022
|
+
}
|
|
1023
|
+
|
|
988
1024
|
if (exclude) {
|
|
989
1025
|
$ = await removeNonContentTags($);
|
|
990
1026
|
$ = await reduceRepeatedDivs($, listLimit);
|
|
1027
|
+
reduceTableRows($, listLimit);
|
|
991
1028
|
// If you later re-enable:
|
|
992
1029
|
// $ = await removeObfuscatedClassNames($);
|
|
993
1030
|
// $ = await removeAutoGeneratedClasses($);
|
|
@@ -1386,7 +1423,15 @@ export class ChromeBrowser {
|
|
|
1386
1423
|
|
|
1387
1424
|
|
|
1388
1425
|
async close() {
|
|
1389
|
-
|
|
1426
|
+
try {
|
|
1427
|
+
if (this.storageStatePath && this.context) {
|
|
1428
|
+
ensureDir(path.dirname(this.storageStatePath));
|
|
1429
|
+
await this.context.storageState({ path: this.storageStatePath, indexedDB: true });
|
|
1430
|
+
this._debugLog('Saved shared authentication state', { path: this.storageStatePath });
|
|
1431
|
+
}
|
|
1432
|
+
} finally {
|
|
1433
|
+
await this.browser?.close();
|
|
1434
|
+
}
|
|
1390
1435
|
}
|
|
1391
1436
|
}
|
|
1392
1437
|
|
package/tools/contextControl.js
CHANGED
|
@@ -44,6 +44,7 @@ export function sanitizeHeavyToolHistory(messages, { keepRecentPerTool = 2 } = {
|
|
|
44
44
|
*
|
|
45
45
|
* Strategy:
|
|
46
46
|
* - Always keep all system messages.
|
|
47
|
+
* - Always keep the latest real user instruction as a protocol anchor.
|
|
47
48
|
* - Start from the end and keep the last `maxNonSystemMessages` non-system messages.
|
|
48
49
|
* - Ensure protocol correctness:
|
|
49
50
|
* * If a tool message is kept, its parent assistant with matching tool_call_id
|
|
@@ -63,6 +64,15 @@ export function pruneConversationContext(messages, { maxNonSystemMessages = 40 }
|
|
|
63
64
|
// Take last N non-system messages (raw window)
|
|
64
65
|
const tail = nonSystem.slice(-maxNonSystemMessages);
|
|
65
66
|
|
|
67
|
+
// Gemini function-calling validation locates the current turn by scanning
|
|
68
|
+
// backward for a normal user message (not a function response). Testronaut's
|
|
69
|
+
// internal tool results use role `tool`, so retain the newest role `user`
|
|
70
|
+
// message even when a long agent loop pushes it outside the rolling tail.
|
|
71
|
+
// This is also useful context for OpenAI and Anthropic: it is the instruction
|
|
72
|
+
// the retained tool activity is attempting to satisfy.
|
|
73
|
+
const latestUser = nonSystem.findLast(m => m.role === 'user');
|
|
74
|
+
const userAnchor = latestUser && !tail.includes(latestUser) ? [latestUser] : [];
|
|
75
|
+
|
|
66
76
|
// Pass 1: build map of tool_call_id -> index of assistant in tail
|
|
67
77
|
const assistantToolParents = new Map(); // tool_call_id -> index in tail
|
|
68
78
|
tail.forEach((m, idx) => {
|
|
@@ -93,7 +103,7 @@ export function pruneConversationContext(messages, { maxNonSystemMessages = 40 }
|
|
|
93
103
|
cleanedTail.push(m);
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
return [...systemMessages, ...cleanedTail];
|
|
106
|
+
return [...systemMessages, ...userAnchor, ...cleanedTail];
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/* ---------------- Ground Control state helpers ----------------
|
package/tools/domControl.js
CHANGED
|
@@ -123,7 +123,7 @@ export const chunkBySection = async ($, focus) => {
|
|
|
123
123
|
footer: $('footer').html() || '',
|
|
124
124
|
aside: $('aside').html() || '',
|
|
125
125
|
body: $('body').html() || ''
|
|
126
|
-
|
|
126
|
+
};
|
|
127
127
|
|
|
128
128
|
if (Array.isArray(focus) && focus.length > 0) {
|
|
129
129
|
return Object.fromEntries(
|
|
@@ -144,6 +144,25 @@ export const chunkBySection = async ($, focus) => {
|
|
|
144
144
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** Keep table headers and only the first N body rows in each table. */
|
|
148
|
+
export function reduceTableRows($, maxAllowed = 3) {
|
|
149
|
+
let limit = maxAllowed === Infinity ? Infinity : Number(maxAllowed);
|
|
150
|
+
if (limit === Infinity) return;
|
|
151
|
+
limit = Number.isFinite(limit) ? Math.max(0, limit) : 3;
|
|
152
|
+
|
|
153
|
+
$('table').each((_, table) => {
|
|
154
|
+
const $table = $(table);
|
|
155
|
+
const rows = $table.find('tbody > tr');
|
|
156
|
+
const candidates = rows.length ? rows : $table.children('tr');
|
|
157
|
+
if (candidates.length <= limit) return;
|
|
158
|
+
candidates.slice(limit).remove();
|
|
159
|
+
const colspan = Math.max(1, $table.find('tr').first().children('th, td').length);
|
|
160
|
+
const marker = `<tr data-collapsed="true"><td colspan="${colspan}">[...truncated]</td></tr>`;
|
|
161
|
+
if (rows.length) $table.children('tbody').first().append(marker);
|
|
162
|
+
else $table.append(marker);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
147
166
|
/**
|
|
148
167
|
* Reduce repeated sibling <div> blocks that share the same class name.
|
|
149
168
|
* - For each parent <body> or <div>, group child divs by `class`.
|
|
@@ -102,7 +102,7 @@ export function generateHtmlReport(report, outputPath) {
|
|
|
102
102
|
${humanInput}
|
|
103
103
|
${planSpan}
|
|
104
104
|
<span class="step-result ${ok ? 'ok' : 'bad'}" ${resultTooltip ? `title="${esc(resultTooltip)}"` : ''}>${esc(resultRaw)}</span>
|
|
105
|
-
<span class="tokens">tokens: ${esc(step.tokensUsed ?? '—')} /
|
|
105
|
+
<span class="tokens">tokens: ${esc(step.tokensUsed ?? '—')} (in: ${esc(step.inputTokens ?? '—')}, out: ${esc(step.outputTokens ?? '—')}) / rolling: ${esc(step.totalTokensUsed ?? '—')}</span>
|
|
106
106
|
</summary>
|
|
107
107
|
<pre class="events">${esc(events.join('\n')) || '(no events)'}</pre>
|
|
108
108
|
${imgTag}
|
package/tools/tokenControl.js
CHANGED
|
@@ -85,13 +85,13 @@ function getTokenizer(model) {
|
|
|
85
85
|
} catch (_) {
|
|
86
86
|
const m = String(model || '').toLowerCase();
|
|
87
87
|
|
|
88
|
-
// Treat Gemini
|
|
89
|
-
const
|
|
88
|
+
// Treat current Gemini generations like modern long-context models.
|
|
89
|
+
const isGemini = /^gemini-(?:2\.5|3(?:\.|-))/.test(m);
|
|
90
90
|
const isClaude = /^claude-/.test(m);
|
|
91
91
|
|
|
92
92
|
// OpenAI modern families also map well to o200k_base
|
|
93
93
|
const useO200k =
|
|
94
|
-
|
|
94
|
+
isGemini ||
|
|
95
95
|
isClaude ||
|
|
96
96
|
m.startsWith('gpt-5') ||
|
|
97
97
|
m.startsWith('gpt-4o') ||
|
package/tools/toolSchema.js
CHANGED
|
@@ -83,6 +83,10 @@ const toolsSchema = [
|
|
|
83
83
|
default: true,
|
|
84
84
|
description: 'Whether to exclude noisy tags like script/style/etc.',
|
|
85
85
|
},
|
|
86
|
+
selector: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Optional CSS selector that scopes the returned DOM to matching elements.',
|
|
89
|
+
},
|
|
86
90
|
},
|
|
87
91
|
},
|
|
88
92
|
},
|
|
@@ -154,21 +158,6 @@ const toolsSchema = [
|
|
|
154
158
|
// }
|
|
155
159
|
// }
|
|
156
160
|
// },
|
|
157
|
-
{
|
|
158
|
-
type: 'function',
|
|
159
|
-
function: {
|
|
160
|
-
name: 'get_dom_chunk',
|
|
161
|
-
description: 'Return a specific chunk of the DOM by index',
|
|
162
|
-
parameters: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: {
|
|
165
|
-
chunkIndex: { type: 'number' },
|
|
166
|
-
totalChunks: { type: 'number' }
|
|
167
|
-
},
|
|
168
|
-
required: ['chunkIndex', 'totalChunks']
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
161
|
{
|
|
173
162
|
type: 'function',
|
|
174
163
|
function: {
|
package/tools/turnLoopUtils.js
CHANGED
|
@@ -3,14 +3,15 @@ import { redactPasswordInText } from '../core/redaction.js';
|
|
|
3
3
|
export const finalResponseHandler = (msg) => {
|
|
4
4
|
const content = String(msg.content ?? '');
|
|
5
5
|
const safeContent = redactPasswordInText(content);
|
|
6
|
-
const final = content.trim().
|
|
7
|
-
|
|
6
|
+
const final = content.trim().replace(/^[#*_`\s]+/, '');
|
|
7
|
+
const verdict = final.match(/^(?:(?:result|final)\s*:\s*)?(success|failure)\b/i)?.[1]?.toLowerCase();
|
|
8
|
+
if (verdict === 'success') {
|
|
8
9
|
console.log('\n┏━ FINAL AGENT RESPONSE ━━━━━━━━━━━━━━━━━━━');
|
|
9
10
|
console.log(safeContent);
|
|
10
11
|
console.log('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
11
12
|
return { finalMessage: safeContent, success: true};
|
|
12
13
|
}
|
|
13
|
-
if (
|
|
14
|
+
if (verdict === 'failure') {
|
|
14
15
|
console.log('\n┏━ FINAL AGENT RESPONSE ━━━━━━━━━━━━━━━━━━━');
|
|
15
16
|
console.log(safeContent);
|
|
16
17
|
console.log('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|