wave-agent-sdk 0.17.1 → 0.17.3
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/builtin/skills/deep-research/SKILL.md +90 -0
- package/builtin/skills/settings/ENV.md +6 -3
- package/dist/agent.d.ts +28 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +128 -34
- package/dist/constants/goalPrompts.d.ts +2 -0
- package/dist/constants/goalPrompts.d.ts.map +1 -0
- package/dist/constants/goalPrompts.js +10 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +1 -0
- package/dist/managers/aiManager.d.ts +7 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +77 -41
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +10 -2
- package/dist/managers/goalManager.d.ts +43 -0
- package/dist/managers/goalManager.d.ts.map +1 -0
- package/dist/managers/goalManager.js +177 -0
- package/dist/managers/messageManager.d.ts +2 -2
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageQueue.d.ts +10 -0
- package/dist/managers/messageQueue.d.ts.map +1 -1
- package/dist/managers/messageQueue.js +53 -1
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +7 -1
- package/dist/managers/skillManager.d.ts +2 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +19 -9
- package/dist/managers/slashCommandManager.d.ts +6 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +105 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +5 -0
- package/dist/managers/workflowManager.d.ts +65 -0
- package/dist/managers/workflowManager.d.ts.map +1 -0
- package/dist/managers/workflowManager.js +380 -0
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +3 -3
- package/dist/services/MarketplaceService.d.ts +2 -2
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +11 -32
- package/dist/services/aiService.d.ts +23 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +102 -9
- package/dist/services/configurationService.d.ts +1 -1
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +3 -16
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +4 -0
- package/dist/services/session.d.ts +9 -1
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +28 -1
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +49 -7
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +1 -1
- package/dist/tools/taskManagementTools.d.ts.map +1 -1
- package/dist/tools/taskManagementTools.js +103 -157
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +0 -9
- package/dist/tools/workflowTool.d.ts +11 -0
- package/dist/tools/workflowTool.d.ts.map +1 -0
- package/dist/tools/workflowTool.js +190 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/commands.d.ts +4 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/config.d.ts +2 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/core.d.ts +1 -1
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +2 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/messaging.d.ts +2 -2
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/processes.d.ts +6 -2
- package/dist/types/processes.d.ts.map +1 -1
- package/dist/types/workflow.d.ts +2 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +1 -0
- package/dist/utils/cacheControlUtils.d.ts +13 -8
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +73 -102
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +7 -0
- package/dist/utils/markdownParser.d.ts.map +1 -1
- package/dist/utils/markdownParser.js +21 -6
- package/dist/utils/messageOperations.d.ts +2 -2
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/notificationXml.d.ts.map +1 -1
- package/dist/workflow/budgetTracker.d.ts +12 -0
- package/dist/workflow/budgetTracker.d.ts.map +1 -0
- package/dist/workflow/budgetTracker.js +30 -0
- package/dist/workflow/concurrencyLimiter.d.ts +14 -0
- package/dist/workflow/concurrencyLimiter.d.ts.map +1 -0
- package/dist/workflow/concurrencyLimiter.js +39 -0
- package/dist/workflow/journal.d.ts +19 -0
- package/dist/workflow/journal.d.ts.map +1 -0
- package/dist/workflow/journal.js +74 -0
- package/dist/workflow/progressReporter.d.ts +21 -0
- package/dist/workflow/progressReporter.d.ts.map +1 -0
- package/dist/workflow/progressReporter.js +118 -0
- package/dist/workflow/runState.d.ts +16 -0
- package/dist/workflow/runState.d.ts.map +1 -0
- package/dist/workflow/runState.js +57 -0
- package/dist/workflow/scriptRuntime.d.ts +35 -0
- package/dist/workflow/scriptRuntime.d.ts.map +1 -0
- package/dist/workflow/scriptRuntime.js +196 -0
- package/dist/workflow/structuredOutput.d.ts +27 -0
- package/dist/workflow/structuredOutput.d.ts.map +1 -0
- package/dist/workflow/structuredOutput.js +106 -0
- package/dist/workflow/types.d.ts +81 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +1 -0
- package/dist/workflow/workflowApis.d.ts +46 -0
- package/dist/workflow/workflowApis.d.ts.map +1 -0
- package/dist/workflow/workflowApis.js +280 -0
- package/package.json +1 -1
- package/src/agent.ts +144 -34
- package/src/constants/goalPrompts.ts +10 -0
- package/src/constants/tools.ts +1 -0
- package/src/managers/aiManager.ts +91 -47
- package/src/managers/backgroundTaskManager.ts +16 -4
- package/src/managers/goalManager.ts +232 -0
- package/src/managers/messageManager.ts +2 -2
- package/src/managers/messageQueue.ts +59 -1
- package/src/managers/pluginManager.ts +8 -1
- package/src/managers/skillManager.ts +20 -9
- package/src/managers/slashCommandManager.ts +119 -0
- package/src/managers/toolManager.ts +7 -0
- package/src/managers/workflowManager.ts +491 -0
- package/src/prompts/index.ts +4 -2
- package/src/services/MarketplaceService.ts +14 -38
- package/src/services/aiService.ts +166 -12
- package/src/services/configurationService.ts +2 -22
- package/src/services/hook.ts +5 -0
- package/src/services/session.ts +42 -2
- package/src/tools/bashTool.ts +64 -9
- package/src/tools/readTool.ts +1 -2
- package/src/tools/taskManagementTools.ts +146 -195
- package/src/tools/types.ts +2 -0
- package/src/tools/webFetchTool.ts +0 -12
- package/src/tools/workflowTool.ts +205 -0
- package/src/types/agent.ts +6 -0
- package/src/types/commands.ts +4 -0
- package/src/types/config.ts +2 -2
- package/src/types/core.ts +3 -3
- package/src/types/hooks.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/messaging.ts +2 -2
- package/src/types/processes.ts +10 -2
- package/src/types/workflow.ts +5 -0
- package/src/utils/cacheControlUtils.ts +106 -131
- package/src/utils/containerSetup.ts +9 -0
- package/src/utils/markdownParser.ts +26 -8
- package/src/utils/messageOperations.ts +2 -2
- package/src/utils/notificationXml.ts +6 -1
- package/src/workflow/budgetTracker.ts +34 -0
- package/src/workflow/concurrencyLimiter.ts +47 -0
- package/src/workflow/journal.ts +95 -0
- package/src/workflow/progressReporter.ts +141 -0
- package/src/workflow/runState.ts +65 -0
- package/src/workflow/scriptRuntime.ts +274 -0
- package/src/workflow/structuredOutput.ts +123 -0
- package/src/workflow/types.ts +95 -0
- package/src/workflow/workflowApis.ts +412 -0
|
@@ -76,6 +76,12 @@ export class AIManager {
|
|
|
76
76
|
private originalWorkdir: string;
|
|
77
77
|
private consecutiveCompactionFailures: number = 0;
|
|
78
78
|
private readonly maxTurns?: number;
|
|
79
|
+
/** Override tool_choice for this AI manager (e.g. for structured output) */
|
|
80
|
+
public toolChoiceOverride?:
|
|
81
|
+
| "auto"
|
|
82
|
+
| "none"
|
|
83
|
+
| "required"
|
|
84
|
+
| { type: "function"; function: { name: string } };
|
|
79
85
|
|
|
80
86
|
// Service overrides
|
|
81
87
|
constructor(
|
|
@@ -613,12 +619,7 @@ export class AIManager {
|
|
|
613
619
|
if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
|
|
614
620
|
}
|
|
615
621
|
|
|
616
|
-
// 2.
|
|
617
|
-
contextParts.push(
|
|
618
|
-
`\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`,
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
// 3. Plan mode context
|
|
622
|
+
// 2. Plan mode context
|
|
622
623
|
const currentMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
623
624
|
this.getModelConfig().permissionMode,
|
|
624
625
|
);
|
|
@@ -894,6 +895,7 @@ export class AIManager {
|
|
|
894
895
|
filteredToolPlugins,
|
|
895
896
|
{
|
|
896
897
|
workdir: this.getWorkdir(),
|
|
898
|
+
originalWorkdir: this.getOriginalWorkdir(),
|
|
897
899
|
memory: combinedMemory,
|
|
898
900
|
language: this.getLanguage(),
|
|
899
901
|
isSubagent: !!this.subagentType,
|
|
@@ -902,6 +904,7 @@ export class AIManager {
|
|
|
902
904
|
},
|
|
903
905
|
), // Pass custom system prompt
|
|
904
906
|
maxTokens: maxTokens, // Pass max tokens override
|
|
907
|
+
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
905
908
|
};
|
|
906
909
|
|
|
907
910
|
// Add streaming callbacks only if streaming is enabled
|
|
@@ -943,35 +946,13 @@ export class AIManager {
|
|
|
943
946
|
};
|
|
944
947
|
}
|
|
945
948
|
|
|
946
|
-
startLLMRequestSpan(model || this.getModelConfig().model);
|
|
947
|
-
const apiStartTime = Date.now();
|
|
948
|
-
let ttftMs: number | undefined;
|
|
949
|
-
|
|
950
|
-
// If streaming, track TTFT via callback
|
|
951
|
-
if (this.stream && callAgentOptions.onContentUpdate) {
|
|
952
|
-
const originalOnContentUpdate = callAgentOptions.onContentUpdate;
|
|
953
|
-
let firstTokenReceived = false;
|
|
954
|
-
callAgentOptions.onContentUpdate = (content: string) => {
|
|
955
|
-
if (!firstTokenReceived) {
|
|
956
|
-
ttftMs = Date.now() - apiStartTime;
|
|
957
|
-
firstTokenReceived = true;
|
|
958
|
-
}
|
|
959
|
-
originalOnContentUpdate(content);
|
|
960
|
-
};
|
|
961
|
-
}
|
|
949
|
+
startLLMRequestSpan(model || this.getModelConfig().model || "");
|
|
962
950
|
|
|
963
951
|
const result = await aiService.callAgent(callAgentOptions);
|
|
964
|
-
const ttltMs = Date.now() - apiStartTime;
|
|
965
952
|
|
|
966
953
|
// End LLM span with usage data
|
|
967
954
|
endLLMRequestSpan({
|
|
968
|
-
model: model || this.getModelConfig().model,
|
|
969
|
-
inputTokens: result.usage?.prompt_tokens,
|
|
970
|
-
outputTokens: result.usage?.completion_tokens,
|
|
971
|
-
cacheReadTokens: result.usage?.cache_read_input_tokens,
|
|
972
|
-
cacheCreationTokens: result.usage?.cache_creation_input_tokens,
|
|
973
|
-
ttftMs,
|
|
974
|
-
ttltMs,
|
|
955
|
+
model: model || this.getModelConfig().model || "",
|
|
975
956
|
success: true,
|
|
976
957
|
hasToolCall: !!(result.tool_calls && result.tool_calls.length > 0),
|
|
977
958
|
});
|
|
@@ -1232,7 +1213,7 @@ export class AIManager {
|
|
|
1232
1213
|
(toolResult.error ? `Error: ${toolResult.error}` : "");
|
|
1233
1214
|
if (jsonRecovered) {
|
|
1234
1215
|
toolResultContent +=
|
|
1235
|
-
"\n\
|
|
1216
|
+
"\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
|
|
1236
1217
|
}
|
|
1237
1218
|
|
|
1238
1219
|
// Update message state - tool execution completed
|
|
@@ -1407,7 +1388,7 @@ export class AIManager {
|
|
|
1407
1388
|
} catch (error) {
|
|
1408
1389
|
// End LLM span with error
|
|
1409
1390
|
endLLMRequestSpan({
|
|
1410
|
-
model: model || this.getModelConfig().model,
|
|
1391
|
+
model: model || this.getModelConfig().model || "",
|
|
1411
1392
|
success: false,
|
|
1412
1393
|
error: error instanceof Error ? error.message : String(error),
|
|
1413
1394
|
});
|
|
@@ -1480,23 +1461,85 @@ export class AIManager {
|
|
|
1480
1461
|
}
|
|
1481
1462
|
}
|
|
1482
1463
|
|
|
1483
|
-
|
|
1464
|
+
// Goal evaluation — supersedes Stop hooks when active
|
|
1465
|
+
const goalManager = this.container.has("GoalManager")
|
|
1466
|
+
? this.container.get<import("./goalManager.js").GoalManager>(
|
|
1467
|
+
"GoalManager",
|
|
1468
|
+
)
|
|
1469
|
+
: undefined;
|
|
1470
|
+
|
|
1471
|
+
let goalContinuing = false;
|
|
1472
|
+
|
|
1473
|
+
if (goalManager?.isGoalActive() && !this.subagentType) {
|
|
1474
|
+
// 1. Increment turn count and check circuit breakers
|
|
1475
|
+
goalManager.incrementTurnCount();
|
|
1476
|
+
const circuitBreaker = goalManager.checkCircuitBreakers();
|
|
1477
|
+
|
|
1478
|
+
if (circuitBreaker) {
|
|
1479
|
+
goalManager.clearGoal();
|
|
1480
|
+
logger?.info(`[Goal] ${circuitBreaker}`);
|
|
1481
|
+
this.messageManager.addUserMessage({
|
|
1482
|
+
content: `<system-reminder>${circuitBreaker}</system-reminder>`,
|
|
1483
|
+
isMeta: true,
|
|
1484
|
+
});
|
|
1485
|
+
// Fall through to normal Stop hooks on the final turn
|
|
1486
|
+
} else {
|
|
1487
|
+
// 2. Evaluate goal
|
|
1488
|
+
const evaluation = await goalManager.evaluateGoal(
|
|
1489
|
+
abortController.signal,
|
|
1490
|
+
);
|
|
1484
1491
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1492
|
+
if (evaluation.isMet) {
|
|
1493
|
+
goalManager.clearGoal();
|
|
1494
|
+
logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
|
|
1495
|
+
this.messageManager.addUserMessage({
|
|
1496
|
+
content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
|
|
1497
|
+
isMeta: true,
|
|
1498
|
+
});
|
|
1499
|
+
// Fall through to normal Stop hooks on the final turn
|
|
1500
|
+
} else {
|
|
1501
|
+
const goal = goalManager.getGoal()!;
|
|
1502
|
+
goal.lastReason = evaluation.reason;
|
|
1503
|
+
logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
|
|
1504
|
+
this.messageManager.addUserMessage({
|
|
1505
|
+
content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
|
|
1506
|
+
isMeta: true,
|
|
1507
|
+
});
|
|
1508
|
+
// Keep loading state active to prevent UI flicker
|
|
1509
|
+
this.setIsLoading(true);
|
|
1510
|
+
goalContinuing = true;
|
|
1511
|
+
await this.sendAIMessage({
|
|
1512
|
+
recursionDepth: 0,
|
|
1513
|
+
model,
|
|
1514
|
+
allowedRules,
|
|
1515
|
+
maxTokens,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1491
1520
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1521
|
+
// Skip Stop hooks when goal evaluator is continuing the conversation
|
|
1522
|
+
if (goalContinuing) {
|
|
1523
|
+
// Goal evaluator supersedes Stop hooks
|
|
1524
|
+
} else {
|
|
1525
|
+
const shouldContinue = await this.executeStopHooks();
|
|
1526
|
+
|
|
1527
|
+
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1528
|
+
// restart the AI conversation cycle
|
|
1529
|
+
if (shouldContinue) {
|
|
1530
|
+
logger?.info(
|
|
1531
|
+
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
// Restart the conversation to let AI fix the issues
|
|
1535
|
+
// Use recursionDepth = 0 to set loading false again for continuation
|
|
1536
|
+
await this.sendAIMessage({
|
|
1537
|
+
recursionDepth: 0,
|
|
1538
|
+
model,
|
|
1539
|
+
allowedRules,
|
|
1540
|
+
maxTokens,
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1500
1543
|
}
|
|
1501
1544
|
}
|
|
1502
1545
|
}
|
|
@@ -1688,6 +1731,7 @@ export class AIManager {
|
|
|
1688
1731
|
toolInput,
|
|
1689
1732
|
toolResponse,
|
|
1690
1733
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1734
|
+
planFilePath: this.permissionManager?.getPlanFilePath(),
|
|
1691
1735
|
env:
|
|
1692
1736
|
this.container.get<Record<string, string>>("MergedEnv") ||
|
|
1693
1737
|
(process.env as Record<string, string>),
|
|
@@ -96,8 +96,14 @@ export class BackgroundTaskManager {
|
|
|
96
96
|
if (child.pid && !child.killed) {
|
|
97
97
|
try {
|
|
98
98
|
process.kill(-child.pid, "SIGKILL");
|
|
99
|
-
} catch (error) {
|
|
100
|
-
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
// ESRCH means the process already exited — not an error
|
|
101
|
+
if (
|
|
102
|
+
!(error instanceof Error) ||
|
|
103
|
+
(error as NodeJS.ErrnoException).code !== "ESRCH"
|
|
104
|
+
) {
|
|
105
|
+
logger.error("Failed to force kill process:", error);
|
|
106
|
+
}
|
|
101
107
|
}
|
|
102
108
|
}
|
|
103
109
|
}, 1000);
|
|
@@ -267,8 +273,14 @@ export class BackgroundTaskManager {
|
|
|
267
273
|
if (child.pid && !child.killed) {
|
|
268
274
|
try {
|
|
269
275
|
process.kill(-child.pid, "SIGKILL");
|
|
270
|
-
} catch (error) {
|
|
271
|
-
|
|
276
|
+
} catch (error: unknown) {
|
|
277
|
+
// ESRCH means the process already exited — not an error
|
|
278
|
+
if (
|
|
279
|
+
!(error instanceof Error) ||
|
|
280
|
+
(error as NodeJS.ErrnoException).code !== "ESRCH"
|
|
281
|
+
) {
|
|
282
|
+
logger.error("Failed to force kill process:", error);
|
|
283
|
+
}
|
|
272
284
|
}
|
|
273
285
|
}
|
|
274
286
|
}, 1000);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Container } from "../utils/container.js";
|
|
2
|
+
import type { MessageManager } from "./messageManager.js";
|
|
3
|
+
import type { AIManager } from "./aiManager.js";
|
|
4
|
+
import type { Usage } from "../types/index.js";
|
|
5
|
+
import { evaluateGoal as aiEvaluateGoal } from "../services/aiService.js";
|
|
6
|
+
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
7
|
+
import { logger } from "../utils/globalLogger.js";
|
|
8
|
+
|
|
9
|
+
export interface GoalState {
|
|
10
|
+
condition: string;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
turnCount: number;
|
|
13
|
+
tokenBaseline: number;
|
|
14
|
+
lastReason?: string;
|
|
15
|
+
consecutiveEvalFailures: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_GOAL_TURNS = 50;
|
|
19
|
+
const MAX_GOAL_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
|
20
|
+
const MAX_CONSECUTIVE_EVAL_FAILURES = 3;
|
|
21
|
+
const MAX_CONDITION_LENGTH = 4000;
|
|
22
|
+
|
|
23
|
+
export class GoalManager {
|
|
24
|
+
private state: GoalState | null = null;
|
|
25
|
+
private onGoalStateChange?: (
|
|
26
|
+
active: boolean,
|
|
27
|
+
condition?: string,
|
|
28
|
+
elapsed?: string,
|
|
29
|
+
) => void;
|
|
30
|
+
private onGoalEvaluating?: (evaluating: boolean) => void;
|
|
31
|
+
|
|
32
|
+
constructor(private container: Container) {}
|
|
33
|
+
|
|
34
|
+
private get messageManager(): MessageManager {
|
|
35
|
+
return this.container.get<MessageManager>("MessageManager")!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private get aiManager(): AIManager {
|
|
39
|
+
return this.container.get<AIManager>("AIManager")!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public setOnGoalStateChange(
|
|
43
|
+
callback: (active: boolean, condition?: string, elapsed?: string) => void,
|
|
44
|
+
): void {
|
|
45
|
+
this.onGoalStateChange = callback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public setOnGoalEvaluating(callback: (evaluating: boolean) => void): void {
|
|
49
|
+
this.onGoalEvaluating = callback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public setGoal(condition: string): void {
|
|
53
|
+
if (condition.length > MAX_CONDITION_LENGTH) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Goal condition exceeds maximum length of ${MAX_CONDITION_LENGTH} characters`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const totalTokens = this.messageManager.getLatestTotalTokens?.() ?? 0;
|
|
60
|
+
|
|
61
|
+
this.state = {
|
|
62
|
+
condition,
|
|
63
|
+
startedAt: Date.now(),
|
|
64
|
+
turnCount: 0,
|
|
65
|
+
tokenBaseline: totalTokens,
|
|
66
|
+
consecutiveEvalFailures: 0,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this.onGoalStateChange?.(true, condition, "0m");
|
|
70
|
+
logger?.info(`[Goal] Set goal: ${condition}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public clearGoal(): void {
|
|
74
|
+
if (this.state) {
|
|
75
|
+
logger?.info(`[Goal] Cleared goal: ${this.state.condition}`);
|
|
76
|
+
this.state = null;
|
|
77
|
+
this.onGoalStateChange?.(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public getGoal(): GoalState | null {
|
|
82
|
+
return this.state;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public isGoalActive(): boolean {
|
|
86
|
+
return this.state !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public incrementTurnCount(): void {
|
|
90
|
+
if (this.state) {
|
|
91
|
+
this.state.turnCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public getStatusString(): string {
|
|
96
|
+
if (!this.state) return "No active goal";
|
|
97
|
+
const elapsed = this.formatElapsed(Date.now() - this.state.startedAt);
|
|
98
|
+
let status = `Goal: ${this.state.condition}\nElapsed: ${elapsed}\nTurns: ${this.state.turnCount}`;
|
|
99
|
+
if (this.state.lastReason) {
|
|
100
|
+
status += `\nLast evaluation: ${this.state.lastReason}`;
|
|
101
|
+
}
|
|
102
|
+
return status;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check circuit breakers. Returns a clear reason if goal should be force-cleared, null otherwise.
|
|
107
|
+
*/
|
|
108
|
+
public checkCircuitBreakers(): string | null {
|
|
109
|
+
if (!this.state) return null;
|
|
110
|
+
|
|
111
|
+
if (this.state.turnCount >= MAX_GOAL_TURNS) {
|
|
112
|
+
return `Goal cancelled: maximum turns (${MAX_GOAL_TURNS}) exceeded`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Date.now() - this.state.startedAt >= MAX_GOAL_DURATION_MS) {
|
|
116
|
+
return `Goal cancelled: time limit (${MAX_GOAL_DURATION_MS / 60000} minutes) exceeded`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.state.consecutiveEvalFailures >= MAX_CONSECUTIVE_EVAL_FAILURES) {
|
|
120
|
+
return `Goal cancelled: ${MAX_CONSECUTIVE_EVAL_FAILURES} consecutive evaluation failures`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Evaluate whether the goal has been met using the fast model.
|
|
128
|
+
*/
|
|
129
|
+
public async evaluateGoal(abortSignal?: AbortSignal): Promise<{
|
|
130
|
+
isMet: boolean;
|
|
131
|
+
reason: string;
|
|
132
|
+
}> {
|
|
133
|
+
if (!this.state) {
|
|
134
|
+
return { isMet: false, reason: "No active goal" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const messages = this.messageManager.getMessages();
|
|
139
|
+
const apiMessages = convertMessagesForAPI(messages);
|
|
140
|
+
const gatewayConfig = this.aiManager.getGatewayConfig();
|
|
141
|
+
const modelConfig = this.aiManager.getModelConfig();
|
|
142
|
+
const fastModel = modelConfig.fastModel || modelConfig.model;
|
|
143
|
+
if (!fastModel) {
|
|
144
|
+
return {
|
|
145
|
+
isMet: false,
|
|
146
|
+
reason: "No model configured for goal evaluation",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.onGoalEvaluating?.(true);
|
|
151
|
+
const result = await aiEvaluateGoal({
|
|
152
|
+
gatewayConfig,
|
|
153
|
+
modelConfig,
|
|
154
|
+
model: fastModel,
|
|
155
|
+
goalCondition: this.state.condition,
|
|
156
|
+
messages: apiMessages,
|
|
157
|
+
abortSignal,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Track evaluation tokens separately
|
|
161
|
+
if (result.usage) {
|
|
162
|
+
const usage: Usage = {
|
|
163
|
+
...result.usage,
|
|
164
|
+
operation_type: "goal_evaluation",
|
|
165
|
+
model: fastModel,
|
|
166
|
+
};
|
|
167
|
+
this.messageManager.addUsage(usage);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Reset failure counter on success
|
|
171
|
+
this.state.consecutiveEvalFailures = 0;
|
|
172
|
+
|
|
173
|
+
this.onGoalEvaluating?.(false);
|
|
174
|
+
|
|
175
|
+
// Parse the response
|
|
176
|
+
return this.parseEvaluationResponse(result.content);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.onGoalEvaluating?.(false);
|
|
179
|
+
this.state.consecutiveEvalFailures++;
|
|
180
|
+
logger?.warn(
|
|
181
|
+
`[Goal] Evaluation failed (${this.state.consecutiveEvalFailures}/${MAX_CONSECUTIVE_EVAL_FAILURES}): ${(error as Error).message}`,
|
|
182
|
+
);
|
|
183
|
+
return {
|
|
184
|
+
isMet: false,
|
|
185
|
+
reason: `Evaluation failed: ${(error as Error).message}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parse the evaluation response from the fast model.
|
|
192
|
+
*/
|
|
193
|
+
private parseEvaluationResponse(content: string): {
|
|
194
|
+
isMet: boolean;
|
|
195
|
+
reason: string;
|
|
196
|
+
} {
|
|
197
|
+
// Try direct JSON parse
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(content);
|
|
200
|
+
if (typeof parsed.met === "boolean") {
|
|
201
|
+
return {
|
|
202
|
+
isMet: parsed.met,
|
|
203
|
+
reason: parsed.reason || "No reason provided",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Fall through to regex
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Try regex extraction
|
|
211
|
+
const metMatch = content.match(/"met"\s*:\s*(true|false)/);
|
|
212
|
+
const reasonMatch = content.match(/"reason"\s*:\s*"([^"]*)"/);
|
|
213
|
+
if (metMatch) {
|
|
214
|
+
return {
|
|
215
|
+
isMet: metMatch[1] === "true",
|
|
216
|
+
reason: reasonMatch?.[1] || "No reason provided",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Default: not met
|
|
221
|
+
return { isMet: false, reason: "Could not parse evaluation response" };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private formatElapsed(ms: number): string {
|
|
225
|
+
const minutes = Math.floor(ms / 60000);
|
|
226
|
+
if (minutes < 1) return "<1m";
|
|
227
|
+
if (minutes < 60) return `${minutes}m`;
|
|
228
|
+
const hours = Math.floor(minutes / 60);
|
|
229
|
+
const remainingMin = minutes % 60;
|
|
230
|
+
return `${hours}h${remainingMin}m`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -62,8 +62,8 @@ export interface MessageManagerCallbacks {
|
|
|
62
62
|
// Notification callback
|
|
63
63
|
onNotificationMessageAdded?: (params: {
|
|
64
64
|
taskId: string;
|
|
65
|
-
taskType: "shell" | "agent";
|
|
66
|
-
status: "completed" | "failed" | "killed";
|
|
65
|
+
taskType: "shell" | "agent" | "workflow";
|
|
66
|
+
status: "completed" | "failed" | "killed" | "aborted";
|
|
67
67
|
summary: string;
|
|
68
68
|
}) => void;
|
|
69
69
|
}
|
|
@@ -1,16 +1,42 @@
|
|
|
1
|
+
export type QueueState = "idle" | "dispatching" | "running";
|
|
2
|
+
|
|
1
3
|
export interface QueuedMessage {
|
|
4
|
+
id?: string;
|
|
2
5
|
type?: "message" | "bang";
|
|
3
6
|
content: string;
|
|
4
7
|
images?: Array<{ path: string; mimeType: string }>;
|
|
5
8
|
longTextMap?: Record<string, string>;
|
|
9
|
+
editable?: boolean; // default true
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export class MessageQueue {
|
|
9
13
|
private queue: QueuedMessage[] = [];
|
|
14
|
+
private nextId = 0;
|
|
15
|
+
private _state: QueueState = "idle";
|
|
10
16
|
onMessageEnqueued?: () => void;
|
|
11
17
|
|
|
18
|
+
get state(): QueueState {
|
|
19
|
+
return this._state;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
transitionTo(newState: QueueState): boolean {
|
|
23
|
+
const valid: Record<QueueState, QueueState[]> = {
|
|
24
|
+
idle: ["dispatching"],
|
|
25
|
+
dispatching: ["running", "idle"],
|
|
26
|
+
running: ["idle"],
|
|
27
|
+
};
|
|
28
|
+
if (!valid[this._state].includes(newState)) return false;
|
|
29
|
+
this._state = newState;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
enqueue(message: QueuedMessage): void {
|
|
13
|
-
|
|
34
|
+
const msg: QueuedMessage = {
|
|
35
|
+
...message,
|
|
36
|
+
id: message.id || `mq-${this.nextId++}`,
|
|
37
|
+
editable: message.editable ?? true,
|
|
38
|
+
};
|
|
39
|
+
this.queue.push(msg);
|
|
14
40
|
this.onMessageEnqueued?.();
|
|
15
41
|
}
|
|
16
42
|
|
|
@@ -20,6 +46,7 @@ export class MessageQueue {
|
|
|
20
46
|
|
|
21
47
|
clear(): void {
|
|
22
48
|
this.queue = [];
|
|
49
|
+
this._state = "idle";
|
|
23
50
|
}
|
|
24
51
|
|
|
25
52
|
hasPending(): boolean {
|
|
@@ -38,4 +65,35 @@ export class MessageQueue {
|
|
|
38
65
|
this.onMessageEnqueued?.();
|
|
39
66
|
return true;
|
|
40
67
|
}
|
|
68
|
+
|
|
69
|
+
removeById(id: string): boolean {
|
|
70
|
+
const index = this.queue.findIndex((m) => m.id === id);
|
|
71
|
+
if (index === -1) return false;
|
|
72
|
+
this.queue.splice(index, 1);
|
|
73
|
+
this.onMessageEnqueued?.();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
popLastEditable(): QueuedMessage | null {
|
|
78
|
+
for (let i = this.queue.length - 1; i >= 0; i--) {
|
|
79
|
+
if (this.queue[i].editable !== false) {
|
|
80
|
+
return this.queue.splice(i, 1)[0] ?? null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
popAllEditable(): QueuedMessage[] {
|
|
87
|
+
const editable: QueuedMessage[] = [];
|
|
88
|
+
const remaining: QueuedMessage[] = [];
|
|
89
|
+
for (const msg of this.queue) {
|
|
90
|
+
if (msg.editable !== false) {
|
|
91
|
+
editable.push(msg);
|
|
92
|
+
} else {
|
|
93
|
+
remaining.push(msg);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.queue = remaining;
|
|
97
|
+
return editable;
|
|
98
|
+
}
|
|
41
99
|
}
|
|
@@ -138,7 +138,14 @@ export class PluginManager {
|
|
|
138
138
|
continue;
|
|
139
139
|
}
|
|
140
140
|
} catch {
|
|
141
|
-
// Manifest read failed
|
|
141
|
+
// Manifest read failed — marketplace may not be cloned yet, try to clone/update it
|
|
142
|
+
try {
|
|
143
|
+
await marketplaceService.updateMarketplace(marketplaceName);
|
|
144
|
+
} catch (updateError) {
|
|
145
|
+
logger?.warn(
|
|
146
|
+
`Failed to clone/update marketplace ${marketplaceName}: ${updateError instanceof Error ? updateError.message : String(updateError)}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
logger?.info(`Auto-installing missing plugin: ${pluginId}`);
|
|
@@ -34,6 +34,8 @@ export class SkillManager extends EventEmitter {
|
|
|
34
34
|
|
|
35
35
|
private skillMetadata = new Map<string, SkillMetadata>();
|
|
36
36
|
private skillContent = new Map<string, Skill>();
|
|
37
|
+
private pluginSkillMetadata = new Map<string, SkillMetadata>();
|
|
38
|
+
private pluginSkillContent = new Map<string, Skill>();
|
|
37
39
|
private initialized = false;
|
|
38
40
|
private fileWatcher: FileWatcherService | null = null;
|
|
39
41
|
private watchEnabled: boolean;
|
|
@@ -77,7 +79,7 @@ export class SkillManager extends EventEmitter {
|
|
|
77
79
|
* Refresh skills by re-discovering them
|
|
78
80
|
*/
|
|
79
81
|
private async refreshSkills(): Promise<void> {
|
|
80
|
-
// Clear
|
|
82
|
+
// Clear only discovered skills (builtin/personal/project), preserve plugin skills
|
|
81
83
|
this.skillMetadata.clear();
|
|
82
84
|
this.skillContent.clear();
|
|
83
85
|
|
|
@@ -94,6 +96,14 @@ export class SkillManager extends EventEmitter {
|
|
|
94
96
|
this.skillMetadata.set(name, skill);
|
|
95
97
|
});
|
|
96
98
|
|
|
99
|
+
// Restore plugin skills
|
|
100
|
+
this.pluginSkillMetadata.forEach((metadata, name) => {
|
|
101
|
+
this.skillMetadata.set(name, metadata);
|
|
102
|
+
});
|
|
103
|
+
this.pluginSkillContent.forEach((skill, name) => {
|
|
104
|
+
this.skillContent.set(name, skill);
|
|
105
|
+
});
|
|
106
|
+
|
|
97
107
|
// Log any discovery errors
|
|
98
108
|
if (discovery.errors.length > 0) {
|
|
99
109
|
logger?.warn(`Found ${discovery.errors.length} skill discovery errors`);
|
|
@@ -382,7 +392,7 @@ export class SkillManager extends EventEmitter {
|
|
|
382
392
|
} catch (error) {
|
|
383
393
|
logger?.error(`Failed to execute skill '${skill_name}':`, error);
|
|
384
394
|
return {
|
|
385
|
-
content:
|
|
395
|
+
content: `**Error executing skill**: ${error instanceof Error ? error.message : String(error)}`,
|
|
386
396
|
};
|
|
387
397
|
}
|
|
388
398
|
}
|
|
@@ -404,14 +414,14 @@ export class SkillManager extends EventEmitter {
|
|
|
404
414
|
|
|
405
415
|
if (!skill) {
|
|
406
416
|
return {
|
|
407
|
-
content:
|
|
417
|
+
content: `**Skill not found**: "${skill_name}"\n\nAvailable skills:\n${this.formatAvailableSkills()}`,
|
|
408
418
|
};
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
if (!skill.isValid) {
|
|
412
422
|
const errorMsg = formatSkillError(skill.skillPath, skill.errors);
|
|
413
423
|
return {
|
|
414
|
-
content:
|
|
424
|
+
content: `**Skill validation failed**:\n\n\`\`\`\n${errorMsg}\n\`\`\``,
|
|
415
425
|
};
|
|
416
426
|
}
|
|
417
427
|
|
|
@@ -420,7 +430,7 @@ export class SkillManager extends EventEmitter {
|
|
|
420
430
|
} catch (error) {
|
|
421
431
|
logger?.error(`Failed to prepare skill '${skill_name}':`, error);
|
|
422
432
|
return {
|
|
423
|
-
content:
|
|
433
|
+
content: `**Error preparing skill**: ${error instanceof Error ? error.message : String(error)}`,
|
|
424
434
|
};
|
|
425
435
|
}
|
|
426
436
|
}
|
|
@@ -429,9 +439,7 @@ export class SkillManager extends EventEmitter {
|
|
|
429
439
|
* Prepare skill content with arguments but without bash execution
|
|
430
440
|
*/
|
|
431
441
|
private prepareSkillContent(skill: Skill, argsString: string): string {
|
|
432
|
-
const
|
|
433
|
-
const description = `*${skill.description}*\n\n`;
|
|
434
|
-
const skillPath = `📁 Skill location: \`${skill.skillPath}\`\n\n`;
|
|
442
|
+
const skillPath = `Base directory for this skill: ${skill.skillPath}\n\n`;
|
|
435
443
|
|
|
436
444
|
// Extract content after frontmatter
|
|
437
445
|
const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
@@ -451,7 +459,7 @@ export class SkillManager extends EventEmitter {
|
|
|
451
459
|
);
|
|
452
460
|
}
|
|
453
461
|
|
|
454
|
-
return
|
|
462
|
+
return skillPath + mainContent;
|
|
455
463
|
}
|
|
456
464
|
|
|
457
465
|
/**
|
|
@@ -508,6 +516,9 @@ export class SkillManager extends EventEmitter {
|
|
|
508
516
|
|
|
509
517
|
this.skillMetadata.set(namespacedName, metadata);
|
|
510
518
|
this.skillContent.set(namespacedName, skill);
|
|
519
|
+
// Also store in plugin-specific maps so they survive refreshSkills()
|
|
520
|
+
this.pluginSkillMetadata.set(namespacedName, metadata);
|
|
521
|
+
this.pluginSkillContent.set(namespacedName, skill);
|
|
511
522
|
}
|
|
512
523
|
logger?.debug(
|
|
513
524
|
`Registered ${skills.length} plugin skills from ${pluginName}. Total skills: ${this.skillMetadata.size}`,
|