Expose stalled local agent failures
This commit is contained in:
1 parent
49d0827650
commit
73d888f4a1
6 files changed
+47
-8
No files matched your search
@@ -419,7 +419,7 @@ describe('Wedged-dispatch timeout backstop', () => {
|
||||
expect(stopped).toBe(true);
|
||||
});
|
||||
|
||||
it('a wedged mission reaches phase advancement once the per-dispatch backstop fires', async () => {
|
||||
it('a wedged mission stalls instead of advancing after required recon dispatches time out', async () => {
|
||||
// Tiny backstop so the test is fast and deterministic.
|
||||
const prev = process.env.T3MP3ST_TASK_TIMEOUT_MS;
|
||||
process.env.T3MP3ST_TASK_TIMEOUT_MS = '30';
|
||||
@@ -442,14 +442,18 @@ describe('Wedged-dispatch timeout backstop', () => {
|
||||
command.start();
|
||||
|
||||
// Poll (real timers): the 1s tick loop must dispatch, wedge, then reap the
|
||||
// dispatch via the backstop and advance past RECON.
|
||||
// dispatch via the backstop. Failed required recon work should stall instead
|
||||
// of advancing the mission with no successful backend/model work.
|
||||
const deadline = Date.now() + 8000;
|
||||
while (Date.now() < deadline && !phaseAdvanced) {
|
||||
while (Date.now() < deadline && !command.getStatus().paused) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
const status = command.getStatus();
|
||||
command.stop();
|
||||
|
||||
expect(phaseAdvanced).toBe(true);
|
||||
expect(phaseAdvanced).toBe(false);
|
||||
expect(status.paused).toBe(true);
|
||||
expect(status.stallReason).toContain('stalled in reconnaissance');
|
||||
// The wedged operator was reset back to idle (or re-tasked in a later phase),
|
||||
// never left stuck in 'executing' with no current task.
|
||||
const stuck = command.cell.getAllOperators().some(
|
||||
|
||||
+6
-2
@@ -65,6 +65,8 @@ export interface AgentResult {
|
||||
durationMs: number;
|
||||
/** Whether the agent hit the iteration limit */
|
||||
hitLimit: boolean;
|
||||
/** Error from the forced final-summary call after hitting limits, if any */
|
||||
finalSummaryError?: string;
|
||||
}
|
||||
|
||||
export interface AgentEvents {
|
||||
@@ -267,6 +269,7 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
||||
});
|
||||
|
||||
let summary = 'Agent reached iteration limit without producing a final summary.';
|
||||
let finalSummaryError: string | undefined;
|
||||
try {
|
||||
const finalResponse = await this.llm.chat(messages, { maxTokens: 2048 });
|
||||
summary = finalResponse.content;
|
||||
@@ -275,8 +278,8 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
||||
for (const f of this.parseFinalFindings(summary)) {
|
||||
if (!allFindings.some((x) => x.title === f.title)) allFindings.push(f);
|
||||
}
|
||||
} catch {
|
||||
// Use what we have
|
||||
} catch (error) {
|
||||
finalSummaryError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
const result: AgentResult = {
|
||||
@@ -288,6 +291,7 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
||||
tokensUsed,
|
||||
durationMs: Date.now() - startTime,
|
||||
hitLimit,
|
||||
finalSummaryError,
|
||||
};
|
||||
|
||||
this.emit('agent:complete', result);
|
||||
|
||||
+7
-1
@@ -268,6 +268,9 @@ import {
|
||||
// TEMPEST COMMAND
|
||||
// =============================================================================
|
||||
|
||||
const DEFAULT_AGENT_MAX_ITERATIONS = 15;
|
||||
const LOCAL_AGENT_MAX_ITERATIONS = Number(process.env.T3MP3ST_LOCAL_AGENT_MAX_ITERATIONS || 30);
|
||||
|
||||
/**
|
||||
* TEMPEST Command - Main orchestration controller
|
||||
*/
|
||||
@@ -1158,8 +1161,11 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
||||
// Attach the agent loop scoped to this archetype's SPECIALIZED role toolkit (defaultTools =
|
||||
// the curated per-operator tool allowlist). toolCategories stays as a coarse fallback.
|
||||
const profile = ARCHETYPE_PROFILES[archetype];
|
||||
const maxIterations = this.llm.getProvider() === 'local-agent'
|
||||
? LOCAL_AGENT_MAX_ITERATIONS
|
||||
: DEFAULT_AGENT_MAX_ITERATIONS;
|
||||
const agentLoop = new AgentLoop(this.llm, this.arsenal, {
|
||||
maxIterations: 15,
|
||||
maxIterations,
|
||||
maxTokens: 50000,
|
||||
toolCategories: profile.toolCategories,
|
||||
tools: profile.defaultTools,
|
||||
|
||||
@@ -461,6 +461,9 @@ export class OperatorAgent extends EventEmitter<OperatorEvents> {
|
||||
return {
|
||||
success: result.success,
|
||||
output: result.summary,
|
||||
error: result.finalSummaryError
|
||||
? `${result.summary} Final summary failed: ${result.finalSummaryError}`
|
||||
: undefined,
|
||||
findings: result.findings.map(f => f.title),
|
||||
nextTasks: undefined,
|
||||
};
|
||||
|
||||
+7
-1
@@ -271,6 +271,7 @@ export class TargetEnvironment extends EventEmitter<TargetEvents> {
|
||||
|
||||
export function createTargetFromUrl(url: string, zone: TargetZone = 'external'): Target {
|
||||
const parsed = new URL(url);
|
||||
const normalizedUrl = parsed.toString();
|
||||
|
||||
return {
|
||||
id: randomUUID(),
|
||||
@@ -278,12 +279,17 @@ export function createTargetFromUrl(url: string, zone: TargetZone = 'external'):
|
||||
type: 'web_application',
|
||||
zone,
|
||||
status: 'discovered',
|
||||
address: url,
|
||||
address: parsed.hostname,
|
||||
port: parsed.port ? parseInt(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),
|
||||
protocol: parsed.protocol.replace(':', ''),
|
||||
services: [],
|
||||
vulnerabilities: [],
|
||||
credentials: [],
|
||||
metadata: {
|
||||
url: normalizedUrl,
|
||||
origin: parsed.origin,
|
||||
path: `${parsed.pathname}${parsed.search}${parsed.hash}`,
|
||||
},
|
||||
discoveredAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user