plugin-agent-orchestrator 1.0.21 → 1.0.23
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/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/214.723affb37c13bf7a.js +10 -0
- package/dist/client-v2/264.0533912e6c5ea2d7.js +10 -0
- package/dist/client-v2/41.1805b2edfaa4afe2.js +10 -0
- package/dist/client-v2/418.5ae055abf141820e.js +10 -0
- package/dist/client-v2/619.d99d3c9e61c99064.js +10 -0
- package/dist/client-v2/70.a15d7fcec7c41768.js +10 -0
- package/dist/client-v2/892.72db4161511c8a16.js +10 -0
- package/dist/client-v2/926.87f660b670d85bcc.js +10 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +7 -6
- package/dist/locale/en-US.json +7 -0
- package/dist/locale/vi-VN.json +7 -0
- package/dist/locale/zh-CN.json +27 -0
- package/dist/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.js +63 -0
- package/dist/server/plugin.js +41 -1
- package/dist/server/services/AgentHarness.js +52 -27
- package/dist/server/services/AgentLoopController.js +8 -2
- package/dist/server/services/AgentLoopService.js +1 -1
- package/dist/server/services/AgentRegistryService.js +53 -42
- package/dist/server/services/CircuitBreaker.js +7 -2
- package/dist/server/services/CodeValidator.js +48 -14
- package/dist/server/services/SandboxRunner.js +18 -14
- package/dist/server/skill-hub/plugin.js +44 -17
- package/dist/server/tools/delegate-task.js +7 -2
- package/dist/server/tools/skill-execute.js +33 -2
- package/dist/server/utils/ai-manager.js +51 -0
- package/dist/server/utils/ctx-utils.js +11 -0
- package/dist/server/utils/skill-settings.js +122 -0
- package/package.json +49 -45
- package/src/client/AIEmployeesContext.tsx +51 -14
- package/src/client/AgentRunsTab.tsx +767 -764
- package/src/client/HarnessProfilesTab.tsx +254 -247
- package/src/client/RulesTab.tsx +780 -716
- package/src/client/TracingTab.tsx +1 -0
- package/src/client/plugin.tsx +34 -27
- package/src/client/skill-hub/components/GitSkillImport.tsx +10 -3
- package/src/client/skill-hub/components/SkillMetrics.tsx +157 -124
- package/src/client/skill-hub/index.tsx +58 -51
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +132 -99
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +71 -58
- package/src/client/tools/registerOrchestratorCards.ts +17 -7
- package/src/client-v2/components/AIEmployeeSelect.tsx +47 -0
- package/src/client-v2/components/AIEmployeesContext.tsx +110 -0
- package/src/client-v2/components/AgentRunsTab.tsx +767 -0
- package/src/client-v2/components/HarnessProfilesTab.tsx +254 -0
- package/src/client-v2/components/RulesTab.tsx +782 -0
- package/src/client-v2/components/TracingTab.tsx +432 -0
- package/src/client-v2/hooks/useApiRequest.ts +114 -0
- package/src/client-v2/pages/AgentRunsPage.tsx +13 -0
- package/src/client-v2/pages/ExecutionHistoryPage.tsx +10 -0
- package/src/client-v2/pages/HarnessProfilesPage.tsx +10 -0
- package/src/client-v2/pages/LoopSettingsPage.tsx +10 -0
- package/src/client-v2/pages/RulesPage.tsx +13 -0
- package/src/client-v2/pages/SkillDefinitionsPage.tsx +10 -0
- package/src/client-v2/pages/SkillMetricsPage.tsx +10 -0
- package/src/client-v2/pages/TracingPage.tsx +13 -0
- package/src/client-v2/plugin.tsx +70 -0
- package/src/client-v2/skill-hub/components/ExecutionHistory.tsx +196 -0
- package/src/client-v2/skill-hub/components/FileLinkList.tsx +37 -0
- package/src/client-v2/skill-hub/components/GitSkillImport.tsx +539 -0
- package/src/client-v2/skill-hub/components/LoopSettings.tsx +331 -0
- package/src/client-v2/skill-hub/components/SkillEditor.tsx +453 -0
- package/src/client-v2/skill-hub/components/SkillManager.tsx +174 -0
- package/src/client-v2/skill-hub/components/SkillMetrics.tsx +157 -0
- package/src/client-v2/skill-hub/components/SkillTestPanel.tsx +135 -0
- package/src/client-v2/skill-hub/locale.ts +13 -0
- package/src/client-v2/skill-hub/tools/loopTemplates.ts +52 -0
- package/src/client-v2/skill-hub/utils/jsonFields.ts +41 -0
- package/src/client-v2/utils/jsonFields.ts +41 -0
- package/src/locale/en-US.json +7 -0
- package/src/locale/vi-VN.json +7 -0
- package/src/locale/zh-CN.json +27 -0
- package/src/server/__tests__/agent-registry-service.test.ts +147 -0
- package/src/server/__tests__/code-validator.test.ts +63 -0
- package/src/server/__tests__/skill-execute.test.ts +33 -0
- package/src/server/__tests__/skill-settings.test.ts +63 -0
- package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +39 -0
- package/src/server/plugin.ts +68 -12
- package/src/server/services/AgentHarness.ts +49 -22
- package/src/server/services/AgentLoopController.ts +17 -6
- package/src/server/services/AgentLoopService.ts +1 -1
- package/src/server/services/AgentPlannerService.ts +10 -0
- package/src/server/services/AgentRegistryService.ts +89 -47
- package/src/server/services/CircuitBreaker.ts +10 -0
- package/src/server/services/CodeValidator.ts +237 -159
- package/src/server/services/SandboxRunner.ts +203 -189
- package/src/server/skill-hub/plugin.ts +933 -898
- package/src/server/tools/delegate-task.ts +12 -9
- package/src/server/tools/skill-execute.ts +194 -160
- package/src/server/utils/ai-manager.ts +24 -0
- package/src/server/utils/ctx-utils.ts +14 -0
- package/src/server/utils/skill-settings.ts +116 -0
- package/dist/client/AIEmployeeSelect.d.ts +0 -11
- package/dist/client/AIEmployeesContext.d.ts +0 -30
- package/dist/client/AgentRunsTab.d.ts +0 -2
- package/dist/client/HarnessProfilesTab.d.ts +0 -2
- package/dist/client/OrchestratorSettings.d.ts +0 -3
- package/dist/client/RulesTab.d.ts +0 -2
- package/dist/client/TracingTab.d.ts +0 -2
- package/dist/client/hooks/useRunEventStream.d.ts +0 -22
- package/dist/client/index.d.ts +0 -2
- package/dist/client/plugin.d.ts +0 -6
- package/dist/client/skill-hub/components/ExecutionHistory.d.ts +0 -2
- package/dist/client/skill-hub/components/ExecutionProgress.d.ts +0 -20
- package/dist/client/skill-hub/components/GitSkillImport.d.ts +0 -7
- package/dist/client/skill-hub/components/LoopSettings.d.ts +0 -2
- package/dist/client/skill-hub/components/SkillEditor.d.ts +0 -7
- package/dist/client/skill-hub/components/SkillManager.d.ts +0 -2
- package/dist/client/skill-hub/components/SkillMetrics.d.ts +0 -2
- package/dist/client/skill-hub/components/SkillTestPanel.d.ts +0 -7
- package/dist/client/skill-hub/index.d.ts +0 -11
- package/dist/client/skill-hub/locale.d.ts +0 -3
- package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +0 -6
- package/dist/client/skill-hub/tools/SkillHubCard.d.ts +0 -3
- package/dist/client/skill-hub/tools/loopTemplates.d.ts +0 -22
- package/dist/client/skill-hub/tools/registerSkillLoopCards.d.ts +0 -1
- package/dist/client/skill-hub/utils/jsonFields.d.ts +0 -3
- package/dist/client/tools/PlanApprovalCard.d.ts +0 -3
- package/dist/client/tools/registerOrchestratorCards.d.ts +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/server/collections/agent-execution-spans.d.ts +0 -9
- package/dist/server/collections/agent-harness-profiles.d.ts +0 -2
- package/dist/server/collections/agent-loop-events.d.ts +0 -2
- package/dist/server/collections/agent-loop-runs.d.ts +0 -2
- package/dist/server/collections/agent-loop-steps.d.ts +0 -2
- package/dist/server/collections/orchestrator-config.d.ts +0 -2
- package/dist/server/collections/orchestrator-logs.d.ts +0 -8
- package/dist/server/collections/skill-definitions.d.ts +0 -3
- package/dist/server/collections/skill-executions.d.ts +0 -3
- package/dist/server/collections/skill-loop-configs.d.ts +0 -3
- package/dist/server/collections/skill-worker-configs.d.ts +0 -3
- package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +0 -4
- package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +0 -4
- package/dist/server/migrations/20260427000000-add-tracing-detail-fields.d.ts +0 -7
- package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +0 -4
- package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +0 -4
- package/dist/server/migrations/20260429000000-add-llm-fields.d.ts +0 -7
- package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +0 -16
- package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +0 -7
- package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.d.ts +0 -7
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.d.ts +0 -12
- package/dist/server/migrations/20260601000000-add-token-fields.d.ts +0 -7
- package/dist/server/plugin.d.ts +0 -16
- package/dist/server/resources/agent-loop.d.ts +0 -3
- package/dist/server/resources/tracing.d.ts +0 -7
- package/dist/server/services/AgentHarness.d.ts +0 -44
- package/dist/server/services/AgentLoopController.d.ts +0 -218
- package/dist/server/services/AgentLoopRepository.d.ts +0 -20
- package/dist/server/services/AgentLoopService.d.ts +0 -159
- package/dist/server/services/AgentPlanValidator.d.ts +0 -4
- package/dist/server/services/AgentPlannerService.d.ts +0 -8
- package/dist/server/services/AgentRegistryService.d.ts +0 -21
- package/dist/server/services/CircuitBreaker.d.ts +0 -40
- package/dist/server/services/CodeValidator.d.ts +0 -32
- package/dist/server/services/ContextAggregator.d.ts +0 -45
- package/dist/server/services/ExecutionSpanService.d.ts +0 -46
- package/dist/server/services/FileManager.d.ts +0 -28
- package/dist/server/services/RunEventBus.d.ts +0 -9
- package/dist/server/services/SandboxRunner.d.ts +0 -41
- package/dist/server/services/SkillManager.d.ts +0 -6
- package/dist/server/services/SkillRepositoryService.d.ts +0 -22
- package/dist/server/services/TokenTracker.d.ts +0 -62
- package/dist/server/services/WorkerEnvManager.d.ts +0 -26
- package/dist/server/skill-hub/actions/git-import.d.ts +0 -21
- package/dist/server/skill-hub/mcp/McpController.d.ts +0 -15
- package/dist/server/skill-hub/plugin.d.ts +0 -61
- package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +0 -16
- package/dist/server/skill-hub/utils/json-fields.d.ts +0 -7
- package/dist/server/tools/agent-loop.d.ts +0 -235
- package/dist/server/tools/delegate-task.d.ts +0 -19
- package/dist/server/tools/external-rag-search.d.ts +0 -42
- package/dist/server/tools/orchestrator-plan.d.ts +0 -205
- package/dist/server/tools/skill-execute.d.ts +0 -36
- package/dist/server/types.d.ts +0 -47
- package/dist/server/utils/ctx-utils.d.ts +0 -30
- package/dist/server/utils/logging.d.ts +0 -6
- /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { CodeValidator, CodeValidationError } from '../services/CodeValidator';
|
|
3
|
+
|
|
4
|
+
const validator = new CodeValidator();
|
|
5
|
+
|
|
6
|
+
describe('CodeValidator.validate — dangerous patterns', () => {
|
|
7
|
+
it('blocks node child_process and dynamic require', () => {
|
|
8
|
+
expect(() => validator.validate(`require('child_process')`, 'node')).toThrow(CodeValidationError);
|
|
9
|
+
expect(() => validator.validate(`const m = 'fs'; require(m);`, 'node')).toThrow(CodeValidationError);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('blocks node indirect eval / Function constructor / dynamic import', () => {
|
|
13
|
+
expect(() => validator.validate(`eval('1+1')`, 'node')).toThrow(CodeValidationError);
|
|
14
|
+
expect(() => validator.validate(`new Function('return 1')()`, 'node')).toThrow(CodeValidationError);
|
|
15
|
+
expect(() => validator.validate(`import('fs')`, 'node')).toThrow(CodeValidationError);
|
|
16
|
+
expect(() => validator.validate(`process.binding('spawn_sync')`, 'node')).toThrow(CodeValidationError);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('blocks python subprocess / socket / importlib / getattr bypass', () => {
|
|
20
|
+
expect(() => validator.validate(`import subprocess`, 'python')).toThrow(CodeValidationError);
|
|
21
|
+
expect(() => validator.validate(`import socket`, 'python')).toThrow(CodeValidationError);
|
|
22
|
+
expect(() => validator.validate(`import importlib`, 'python')).toThrow(CodeValidationError);
|
|
23
|
+
expect(() => validator.validate(`getattr(os, 'sys' + 'tem')('id')`, 'python')).toThrow(CodeValidationError);
|
|
24
|
+
expect(() => validator.validate(`__import__('os')`, 'python')).toThrow(CodeValidationError);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('allows benign code', () => {
|
|
28
|
+
expect(() => validator.validate(`const fs = require('fs'); fs.writeFileSync('x', '1');`, 'node')).not.toThrow();
|
|
29
|
+
expect(() => validator.validate(`import json\nprint(json.dumps({'a': 1}))`, 'python')).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('CodeValidator.validateImports — allowlist', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
delete process.env.SKILL_HUB_ALLOW_ANY_IMPORT;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('blocks stdlib network modules even when whitelist is empty (exfiltration hole)', () => {
|
|
39
|
+
// urllib / ftplib are stdlib (no install needed) and must NOT pass on empty whitelist.
|
|
40
|
+
expect(() => validator.validateImports(`import urllib.request`, 'python', [])).toThrow(CodeValidationError);
|
|
41
|
+
expect(() => validator.validateImports(`import ftplib`, 'python', [])).toThrow(CodeValidationError);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('allows builtins on empty whitelist', () => {
|
|
45
|
+
expect(() => validator.validateImports(`import json`, 'python', [])).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('allows whitelisted package and maps PyPI→import name', () => {
|
|
49
|
+
expect(() => validator.validateImports(`import requests`, 'python', ['requests'])).not.toThrow();
|
|
50
|
+
expect(() => validator.validateImports(`from docx import Document`, 'python', ['python-docx'])).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('blocks non-whitelisted node package but allows builtins', () => {
|
|
54
|
+
expect(() => validator.validateImports(`require('lodash')`, 'node', [])).toThrow(CodeValidationError);
|
|
55
|
+
expect(() => validator.validateImports(`require('lodash')`, 'node', ['lodash'])).not.toThrow();
|
|
56
|
+
expect(() => validator.validateImports(`require('path')`, 'node', [])).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('honours SKILL_HUB_ALLOW_ANY_IMPORT escape hatch on empty whitelist', () => {
|
|
60
|
+
process.env.SKILL_HUB_ALLOW_ANY_IMPORT = 'true';
|
|
61
|
+
expect(() => validator.validateImports(`import urllib.request`, 'python', [])).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createSkillExecuteTool } from '../tools/skill-execute';
|
|
3
|
+
|
|
4
|
+
describe('createSkillExecuteTool', () => {
|
|
5
|
+
it('bridges ToolsRuntime onto ctx.runtime while executing a skill', async () => {
|
|
6
|
+
const runtime = { toolCallId: 'tool-1', writer: vi.fn() };
|
|
7
|
+
const ctx: { runtime?: typeof runtime } = {};
|
|
8
|
+
const skill = { get: vi.fn((name: string) => (name === 'name' ? 'demo' : undefined)) };
|
|
9
|
+
const executeSkill = vi.fn(async (_skill, _input, executeCtx) => {
|
|
10
|
+
expect(executeCtx.runtime).toBe(runtime);
|
|
11
|
+
return {
|
|
12
|
+
status: 'succeeded',
|
|
13
|
+
files: [],
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
const plugin = {
|
|
17
|
+
app: { logger: { info: vi.fn() } },
|
|
18
|
+
db: {
|
|
19
|
+
getRepository: vi.fn(() => ({
|
|
20
|
+
findOne: vi.fn(async () => skill),
|
|
21
|
+
})),
|
|
22
|
+
},
|
|
23
|
+
executeSkill,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const tool = createSkillExecuteTool(plugin);
|
|
27
|
+
const result = await tool.invoke(ctx, { action: 'execute', skillName: 'demo', input: { ok: true } }, runtime);
|
|
28
|
+
|
|
29
|
+
expect(result.status).toBe('success');
|
|
30
|
+
expect(executeSkill).toHaveBeenCalledOnce();
|
|
31
|
+
expect(ctx.runtime).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeAIEmployeeSkillSettings } from '../utils/skill-settings';
|
|
3
|
+
|
|
4
|
+
describe('normalizeAIEmployeeSkillSettings', () => {
|
|
5
|
+
it('moves legacy object tool bindings from skills to tools', () => {
|
|
6
|
+
const result = normalizeAIEmployeeSkillSettings({
|
|
7
|
+
skills: [{ name: 'orchestrator_plan_goal', autoCall: false }, 'crm-research'],
|
|
8
|
+
tools: [],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(result.changed).toBe(true);
|
|
12
|
+
expect(result.skillSettings.skills).toEqual(['crm-research']);
|
|
13
|
+
expect(result.skillSettings.tools).toEqual([{ name: 'orchestrator_plan_goal', autoCall: false }]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('moves orchestrator tool names stored as skill strings to tools', () => {
|
|
17
|
+
const result = normalizeAIEmployeeSkillSettings({
|
|
18
|
+
skills: ['delegate_lead_to_researcher', 'crm-research'],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result.changed).toBe(true);
|
|
22
|
+
expect(result.skillSettings.skills).toEqual(['crm-research']);
|
|
23
|
+
expect(result.skillSettings.tools).toEqual([{ name: 'delegate_lead_to_researcher', autoCall: false }]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('keeps current tool bindings and avoids duplicate migrated tools', () => {
|
|
27
|
+
const result = normalizeAIEmployeeSkillSettings({
|
|
28
|
+
skills: [{ name: 'dispatch_subagents_lead', autoCall: true }],
|
|
29
|
+
tools: [{ name: 'dispatch_subagents_lead', autoCall: false }],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.changed).toBe(true);
|
|
33
|
+
expect(result.skillSettings.skills).toEqual([]);
|
|
34
|
+
expect(result.skillSettings.tools).toEqual([{ name: 'dispatch_subagents_lead', autoCall: false }]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('moves browser and drawio tools stored as skill strings to tools', () => {
|
|
38
|
+
const result = normalizeAIEmployeeSkillSettings({
|
|
39
|
+
skills: ['browser_open_url', 'drawio-edit-diagram', 'crm-research'],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.changed).toBe(true);
|
|
43
|
+
expect(result.skillSettings.skills).toEqual(['crm-research']);
|
|
44
|
+
expect(result.skillSettings.tools).toEqual([
|
|
45
|
+
{ name: 'browser_open_url', autoCall: false },
|
|
46
|
+
{ name: 'drawio-edit-diagram', autoCall: false },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('moves Skill Hub tool bindings from skills to tools', () => {
|
|
51
|
+
const result = normalizeAIEmployeeSkillSettings({
|
|
52
|
+
skills: ['skill_hub_execute', { name: 'skill_hub_generate_report', autoCall: true }],
|
|
53
|
+
tools: [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.changed).toBe(true);
|
|
57
|
+
expect(result.skillSettings.skills).toEqual([]);
|
|
58
|
+
expect(result.skillSettings.tools).toEqual([
|
|
59
|
+
{ name: 'skill_hub_execute', autoCall: false },
|
|
60
|
+
{ name: 'skill_hub_generate_report', autoCall: true },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from '@nocobase/server';
|
|
2
|
+
import { normalizeAIEmployeeSkillSettings } from '../utils/skill-settings';
|
|
3
|
+
|
|
4
|
+
export default class NormalizeAIEmployeeToolBindings extends Migration {
|
|
5
|
+
on = 'afterLoad';
|
|
6
|
+
appVersion = '>=0.1.0';
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
const repo = (this as unknown as { db: { getRepository: (name: string) => any }; app?: any }).db.getRepository(
|
|
10
|
+
'aiEmployees',
|
|
11
|
+
);
|
|
12
|
+
if (!repo) return;
|
|
13
|
+
|
|
14
|
+
const rows = await repo.find({});
|
|
15
|
+
let updated = 0;
|
|
16
|
+
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
const skillSettings = row.get?.('skillSettings') ?? row.skillSettings;
|
|
19
|
+
const normalized = normalizeAIEmployeeSkillSettings(skillSettings);
|
|
20
|
+
if (!normalized.changed) continue;
|
|
21
|
+
|
|
22
|
+
await row.update({
|
|
23
|
+
skillSettings: normalized.skillSettings,
|
|
24
|
+
});
|
|
25
|
+
updated += 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (updated > 0) {
|
|
29
|
+
(this as unknown as { app?: { logger?: { info?: (message: string) => void } } }).app?.logger?.info?.(
|
|
30
|
+
`[AgentOrchestrator] Normalized AI employee tool bindings (${updated}).`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async down() {
|
|
36
|
+
// No rollback: this only moves tool-shaped entries from skillSettings.skills
|
|
37
|
+
// into the current NocoBase skillSettings.tools shape.
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/server/plugin.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { registerAgentLoopResource } from './resources/agent-loop';
|
|
|
8
8
|
import { getRunEventBus } from './services/RunEventBus';
|
|
9
9
|
import SkillHubSubFeature from './skill-hub/plugin';
|
|
10
10
|
import { AgentLoopService } from './services/AgentLoopService';
|
|
11
|
+
import { isAdminUser, currentUserId } from './utils/ctx-utils';
|
|
12
|
+
import { getAIToolsManager } from './utils/ai-manager';
|
|
11
13
|
|
|
12
14
|
export class PluginAgentOrchestratorServer extends Plugin {
|
|
13
15
|
skillHub: SkillHubSubFeature;
|
|
@@ -20,10 +22,10 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
20
22
|
|
|
21
23
|
async beforeLoad() {
|
|
22
24
|
// Import collection definitions
|
|
23
|
-
|
|
25
|
+
this.db.import({ directory: path.resolve(__dirname, 'collections') });
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
namespace:
|
|
27
|
+
this.db.addMigrations({
|
|
28
|
+
namespace: this.name,
|
|
27
29
|
directory: path.resolve(__dirname, 'migrations'),
|
|
28
30
|
context: { plugin: this },
|
|
29
31
|
});
|
|
@@ -33,8 +35,8 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
33
35
|
await this.skillHub.load();
|
|
34
36
|
|
|
35
37
|
// --- ACL ---
|
|
36
|
-
|
|
37
|
-
name: `pm.${
|
|
38
|
+
this.app.acl.registerSnippet({
|
|
39
|
+
name: `pm.${this.name}`,
|
|
38
40
|
actions: [
|
|
39
41
|
'orchestratorConfig:*',
|
|
40
42
|
'orchestratorTracing:*',
|
|
@@ -53,11 +55,46 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
53
55
|
],
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
// Allow any logged-in user to read available skills and loop configs.
|
|
59
|
+
// This mirrors the plugin-ai pattern (acl.allow with 'loggedIn')
|
|
60
|
+
// so that non-admin users with AI roles can use skills without
|
|
61
|
+
// requiring manual snippet assignment per role.
|
|
62
|
+
// Create/update/destroy remain restricted to admin roles via the snippet above.
|
|
63
|
+
this.app.acl.allow('skillDefinitions', 'list', 'loggedIn');
|
|
64
|
+
this.app.acl.allow('skillDefinitions', 'get', 'loggedIn');
|
|
65
|
+
this.app.acl.allow('skillLoopConfigs', 'list', 'loggedIn');
|
|
66
|
+
this.app.acl.allow('skillLoopConfigs', 'get', 'loggedIn');
|
|
67
|
+
this.app.acl.allow('skillExecutions', 'list', 'loggedIn');
|
|
68
|
+
this.app.acl.allow('skillExecutions', 'get', 'loggedIn');
|
|
69
|
+
this.app.acl.allow('skillHub', 'test', 'loggedIn');
|
|
70
|
+
this.app.acl.allow('skillHub', 'download', 'loggedIn');
|
|
71
|
+
this.app.acl.allow('skillHub', 'listTemplates', 'loggedIn');
|
|
72
|
+
|
|
73
|
+
// Data scoping for skillExecutions: a logged-in non-admin user may only
|
|
74
|
+
// read their own executions. Rows hold inputArgs / stdout / output files,
|
|
75
|
+
// so an unscoped list/get would leak one user's data to another. Admins
|
|
76
|
+
// (root/admin roles) keep full visibility. This mirrors the owner/admin
|
|
77
|
+
// check enforced by skillHub:download.
|
|
78
|
+
this.app.resourceManager.use(
|
|
79
|
+
async (ctx, next) => {
|
|
80
|
+
const { resourceName, actionName } = ctx.action || {};
|
|
81
|
+
if (resourceName === 'skillExecutions' && (actionName === 'list' || actionName === 'get')) {
|
|
82
|
+
if (!isAdminUser(ctx)) {
|
|
83
|
+
const userId = currentUserId(ctx);
|
|
84
|
+
const ownerFilter = userId ? { triggeredById: userId } : { triggeredById: null };
|
|
85
|
+
ctx.action.mergeParams({ filter: ownerFilter });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await next();
|
|
89
|
+
},
|
|
90
|
+
{ tag: 'orchestrator-skill-executions-scope', after: 'acl' },
|
|
91
|
+
);
|
|
92
|
+
|
|
56
93
|
// --- Register Dynamic Tools ---
|
|
57
94
|
// Each configured sub-agent becomes a callable tool for its leader.
|
|
58
95
|
// Uses createReactAgent (LangGraph public API) instead of private AIEmployee class.
|
|
59
96
|
// Tools are registered via app.aiManager.toolsManager (public API from @nocobase/ai core).
|
|
60
|
-
const toolsManager = (this
|
|
97
|
+
const toolsManager = getAIToolsManager(this.app);
|
|
61
98
|
toolsManager.registerTools(createOrchestratorPlanTools(this, this.agentLoopService));
|
|
62
99
|
toolsManager.registerTools(createExternalRagSearchTool(this));
|
|
63
100
|
toolsManager.registerDynamicTools(createDelegateToolsProvider(this));
|
|
@@ -66,7 +103,7 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
66
103
|
registerAgentLoopResource(this, this.agentLoopService);
|
|
67
104
|
|
|
68
105
|
// --- Register SSE Event Stream Resource (Phase 6) ---
|
|
69
|
-
|
|
106
|
+
this.app.resource({
|
|
70
107
|
name: 'agentLoopEventsStream',
|
|
71
108
|
actions: {
|
|
72
109
|
async stream(ctx, next) {
|
|
@@ -76,6 +113,25 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
76
113
|
return;
|
|
77
114
|
}
|
|
78
115
|
|
|
116
|
+
// Ownership check: a non-admin user may only stream events for a run
|
|
117
|
+
// they started. Run events can echo step inputs/outputs, so an
|
|
118
|
+
// unscoped stream would leak another user's run activity.
|
|
119
|
+
if (!isAdminUser(ctx)) {
|
|
120
|
+
const userId = currentUserId(ctx);
|
|
121
|
+
const run = await ctx.db.getRepository('agentLoopRuns').findOne({
|
|
122
|
+
filter: { id: runId },
|
|
123
|
+
});
|
|
124
|
+
if (!run) {
|
|
125
|
+
ctx.throw(404, 'Run not found.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const ownerId = run.get ? run.get('userId') : run.userId;
|
|
129
|
+
if (!userId || String(ownerId) !== String(userId)) {
|
|
130
|
+
ctx.throw(403, 'You cannot stream events for this run.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
ctx.type = 'text/event-stream';
|
|
80
136
|
ctx.set('Cache-Control', 'no-cache');
|
|
81
137
|
ctx.set('Connection', 'keep-alive');
|
|
@@ -123,15 +179,15 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
123
179
|
// --- Log Retention ---
|
|
124
180
|
// Daily prune of orchestratorLogs / agentExecutionSpans to keep tables bounded.
|
|
125
181
|
// Override window via env: ORCHESTRATOR_LOG_RETENTION_DAYS (default 30).
|
|
126
|
-
|
|
182
|
+
this.app.cronJobManager.addJob({
|
|
127
183
|
cronTime: '0 30 2 * * *',
|
|
128
184
|
onTick: async () => {
|
|
129
185
|
try {
|
|
130
186
|
const days = Number(process.env.ORCHESTRATOR_LOG_RETENTION_DAYS || 30);
|
|
131
187
|
if (!Number.isFinite(days) || days <= 0) return;
|
|
132
188
|
const cutoff = new Date(Date.now() - days * 86400000);
|
|
133
|
-
const repo =
|
|
134
|
-
const spansRepo =
|
|
189
|
+
const repo = this.db.getRepository('orchestratorLogs');
|
|
190
|
+
const spansRepo = this.db.getRepository('agentExecutionSpans');
|
|
135
191
|
const deletedLogs = repo
|
|
136
192
|
? await repo.destroy({
|
|
137
193
|
filter: { createdAt: { $lt: cutoff.toISOString() } },
|
|
@@ -142,11 +198,11 @@ export class PluginAgentOrchestratorServer extends Plugin {
|
|
|
142
198
|
filter: { createdAt: { $lt: cutoff.toISOString() } },
|
|
143
199
|
})
|
|
144
200
|
: 0;
|
|
145
|
-
|
|
201
|
+
this.app.log.info(
|
|
146
202
|
`[AgentOrchestrator] Pruned ${deletedLogs} orchestratorLogs and ${deletedSpans} agentExecutionSpans rows older than ${days} day(s).`,
|
|
147
203
|
);
|
|
148
204
|
} catch (e) {
|
|
149
|
-
|
|
205
|
+
this.app.log.error('[AgentOrchestrator] Log retention job failed', e);
|
|
150
206
|
}
|
|
151
207
|
},
|
|
152
208
|
});
|
|
@@ -7,10 +7,13 @@ import { ExecutionSpanService, setOrchestratorTraceContext } from './ExecutionSp
|
|
|
7
7
|
import { AgentRegistryService } from './AgentRegistryService';
|
|
8
8
|
import { TokenTracker, extractTokenUsage } from './TokenTracker';
|
|
9
9
|
import { ContextAggregator } from './ContextAggregator';
|
|
10
|
-
import { getCircuitBreaker } from './CircuitBreaker';
|
|
10
|
+
import { getCircuitBreaker, subAgentCircuitKey } from './CircuitBreaker';
|
|
11
11
|
import { toPlain, asObject, trimText, nowIso } from '../utils/ctx-utils';
|
|
12
|
+
import { normalizeAIEmployeeSkillSettings } from '../utils/skill-settings';
|
|
12
13
|
import { logDelegation as sharedLogDelegation } from '../utils/logging';
|
|
14
|
+
import { getAIToolsManager, tryGetAIToolsManager } from '../utils/ai-manager';
|
|
13
15
|
import { TraceEvent } from '../types';
|
|
16
|
+
import type { ToolsRuntime } from '@nocobase/ai';
|
|
14
17
|
|
|
15
18
|
const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
|
|
16
19
|
const ORCHESTRATOR_PATH_KEY = '__orchestratorPath';
|
|
@@ -31,9 +34,10 @@ export class AgentHarness {
|
|
|
31
34
|
constructor(
|
|
32
35
|
private readonly plugin: any,
|
|
33
36
|
private readonly registryService: AgentRegistryService,
|
|
37
|
+
tokenTracker?: TokenTracker,
|
|
34
38
|
) {
|
|
35
39
|
this.spanService = new ExecutionSpanService(plugin);
|
|
36
|
-
this.tokenTracker = new TokenTracker(plugin);
|
|
40
|
+
this.tokenTracker = tokenTracker || new TokenTracker(plugin);
|
|
37
41
|
this.contextAggregator = new ContextAggregator(plugin);
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -112,10 +116,11 @@ export class AgentHarness {
|
|
|
112
116
|
);
|
|
113
117
|
|
|
114
118
|
const circuitBreaker = getCircuitBreaker({ appLog: this.app });
|
|
119
|
+
const circuitKey = subAgentCircuitKey(run.leaderUsername, target);
|
|
115
120
|
|
|
116
121
|
// ── Circuit breaker check before invoking sub-agent ──────────────
|
|
117
|
-
if (!circuitBreaker.isAllowed(
|
|
118
|
-
const state = circuitBreaker.getState(
|
|
122
|
+
if (!circuitBreaker.isAllowed(circuitKey)) {
|
|
123
|
+
const state = circuitBreaker.getState(circuitKey);
|
|
119
124
|
throw new Error(
|
|
120
125
|
`Sub-agent "${target}" circuit is open (${state?.failures || 0} failures). Retry after the recovery timeout.`,
|
|
121
126
|
);
|
|
@@ -123,12 +128,12 @@ export class AgentHarness {
|
|
|
123
128
|
|
|
124
129
|
try {
|
|
125
130
|
const result = await this.invokeNamedTool(run, step, toolName, { task, context }, settings, options);
|
|
126
|
-
circuitBreaker.recordSuccess(
|
|
131
|
+
circuitBreaker.recordSuccess(circuitKey);
|
|
127
132
|
return result;
|
|
128
133
|
} catch (e: any) {
|
|
129
134
|
// Don't count approval pauses as circuit failures
|
|
130
135
|
if (e?.message !== 'requires_approval') {
|
|
131
|
-
circuitBreaker.recordFailure(
|
|
136
|
+
circuitBreaker.recordFailure(circuitKey);
|
|
132
137
|
}
|
|
133
138
|
throw e;
|
|
134
139
|
}
|
|
@@ -157,7 +162,7 @@ export class AgentHarness {
|
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
const toolsManager = this.app
|
|
165
|
+
const toolsManager = tryGetAIToolsManager(this.app);
|
|
161
166
|
const tool = await toolsManager?.getTools?.(toolName);
|
|
162
167
|
if (!tool?.invoke) {
|
|
163
168
|
throw new Error(`Tool "${toolName}" was not found or is missing standard invoke handler.`);
|
|
@@ -184,8 +189,18 @@ export class AgentHarness {
|
|
|
184
189
|
ctx.state = { ...(ctx.state || {}), currentAIEmployee: run.leaderUsername };
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
const previousRuntime = ctx.runtime;
|
|
193
|
+
const toolCallId = `agent-loop-${run.id}-${step.id}`;
|
|
194
|
+
const runtime: ToolsRuntime = {
|
|
195
|
+
toolCallId,
|
|
196
|
+
writer: (chunk: any) => {
|
|
197
|
+
this.app.log.trace(`[AgentHarness] Tool output:`, chunk);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
ctx.runtime = runtime;
|
|
201
|
+
|
|
187
202
|
try {
|
|
188
|
-
const result = await tool.invoke(ctx, args,
|
|
203
|
+
const result = await tool.invoke(ctx, args, runtime);
|
|
189
204
|
if (result?.status === 'error') {
|
|
190
205
|
throw new Error(result.content || `Tool "${toolName}" returned an error.`);
|
|
191
206
|
}
|
|
@@ -195,6 +210,11 @@ export class AgentHarness {
|
|
|
195
210
|
result,
|
|
196
211
|
};
|
|
197
212
|
} finally {
|
|
213
|
+
if (previousRuntime === undefined) {
|
|
214
|
+
delete ctx.runtime;
|
|
215
|
+
} else {
|
|
216
|
+
ctx.runtime = previousRuntime;
|
|
217
|
+
}
|
|
198
218
|
if (previousEmployee === undefined) {
|
|
199
219
|
delete ctx._currentAIEmployee;
|
|
200
220
|
} else {
|
|
@@ -251,6 +271,7 @@ export class AgentHarness {
|
|
|
251
271
|
agentLoopRunId,
|
|
252
272
|
agentLoopStepId,
|
|
253
273
|
} = options;
|
|
274
|
+
const effectiveRootRunId = rootRunId || `run_${Date.now()}`;
|
|
254
275
|
|
|
255
276
|
const startTime = Date.now();
|
|
256
277
|
const currentDepth = options.currentDepth ?? 0;
|
|
@@ -265,7 +286,7 @@ export class AgentHarness {
|
|
|
265
286
|
];
|
|
266
287
|
|
|
267
288
|
const executionSpan = await this.spanService.create({
|
|
268
|
-
rootRunId:
|
|
289
|
+
rootRunId: effectiveRootRunId,
|
|
269
290
|
parentSpanId,
|
|
270
291
|
type: 'sub_agent',
|
|
271
292
|
status: 'running',
|
|
@@ -323,24 +344,21 @@ export class AgentHarness {
|
|
|
323
344
|
});
|
|
324
345
|
const chatModel = provider.createModel();
|
|
325
346
|
|
|
326
|
-
const coreToolsManager = ctx.app
|
|
347
|
+
const coreToolsManager = getAIToolsManager(ctx.app);
|
|
327
348
|
const allTools = await coreToolsManager.listTools();
|
|
328
349
|
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
333
|
-
.filter((s: any) => Boolean(s.name));
|
|
334
|
-
const employeeSkillMap = new Map<string, any>(employeeSkills.map((s: any) => [s.name, s]));
|
|
350
|
+
const normalizedSkillSettings = normalizeAIEmployeeSkillSettings(subAgentEmployee.skillSettings).skillSettings;
|
|
351
|
+
const employeeTools = normalizedSkillSettings.tools.filter((tool) => Boolean(tool.name));
|
|
352
|
+
const employeeToolMap = new Map(employeeTools.map((tool) => [tool.name, tool]));
|
|
335
353
|
|
|
336
354
|
const langchainTools: DynamicStructuredTool[] = [];
|
|
337
355
|
|
|
338
356
|
for (const toolEntry of allTools) {
|
|
339
357
|
const entryName = toolEntry.definition.name;
|
|
340
358
|
if (!entryName) continue;
|
|
341
|
-
const
|
|
359
|
+
const employeeTool = employeeToolMap.get(entryName);
|
|
342
360
|
|
|
343
|
-
if (!
|
|
361
|
+
if (!employeeTool || employeeTool.autoCall !== true || toolEntry.execution === 'frontend') {
|
|
344
362
|
continue;
|
|
345
363
|
}
|
|
346
364
|
|
|
@@ -362,7 +380,7 @@ export class AgentHarness {
|
|
|
362
380
|
const toolStartedAt = Date.now();
|
|
363
381
|
const isSkillHubTool = entryName === 'skill_hub_execute' || entryName.startsWith('skill_hub_');
|
|
364
382
|
const toolSpan = await this.spanService.create({
|
|
365
|
-
rootRunId:
|
|
383
|
+
rootRunId: effectiveRootRunId,
|
|
366
384
|
parentSpanId: executionSpanId,
|
|
367
385
|
type: isSkillHubTool ? 'skill' : 'tool',
|
|
368
386
|
status: 'running',
|
|
@@ -381,7 +399,7 @@ export class AgentHarness {
|
|
|
381
399
|
});
|
|
382
400
|
const toolSpanId = toolSpan?.id ? String(toolSpan.id) : undefined;
|
|
383
401
|
setOrchestratorTraceContext(invokeCtx, {
|
|
384
|
-
rootRunId,
|
|
402
|
+
rootRunId: effectiveRootRunId,
|
|
385
403
|
spanId: toolSpanId,
|
|
386
404
|
parentSpanId: executionSpanId,
|
|
387
405
|
toolCallId: `orch-${toolCallId}`,
|
|
@@ -400,8 +418,17 @@ export class AgentHarness {
|
|
|
400
418
|
args: toolArgs,
|
|
401
419
|
});
|
|
402
420
|
|
|
421
|
+
const subToolCallId = `orch-${toolCallId}`;
|
|
422
|
+
const runtime: ToolsRuntime = {
|
|
423
|
+
toolCallId: subToolCallId,
|
|
424
|
+
writer: (chunk: any) => {
|
|
425
|
+
this.app.log.trace(`[AgentHarness] Tool output:`, chunk);
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
invokeCtx.runtime = runtime;
|
|
429
|
+
|
|
403
430
|
try {
|
|
404
|
-
const res = await toolEntry.invoke(invokeCtx, toolArgs,
|
|
431
|
+
const res = await toolEntry.invoke(invokeCtx, toolArgs, runtime);
|
|
405
432
|
const output = trimText(res?.content ?? res?.result ?? res, 50000);
|
|
406
433
|
trace.push({
|
|
407
434
|
type: 'tool_result',
|
|
@@ -461,7 +488,7 @@ export class AgentHarness {
|
|
|
461
488
|
const sessionId =
|
|
462
489
|
ctx.action?.params?.values?.sessionId || ctx.action?.params?.sessionId || ctx.state?.sessionId;
|
|
463
490
|
const contextSummary = await kbPlugin.sessionContext.buildSummary(
|
|
464
|
-
{ rootRunId, ...(sessionId ? { sessionId } : {}) },
|
|
491
|
+
{ rootRunId: effectiveRootRunId, ...(sessionId ? { sessionId } : {}) },
|
|
465
492
|
6000,
|
|
466
493
|
);
|
|
467
494
|
if (contextSummary) {
|
|
@@ -5,7 +5,7 @@ import { AgentLoopRepository } from './AgentLoopRepository';
|
|
|
5
5
|
import { AgentHarness } from './AgentHarness';
|
|
6
6
|
import { AgentLoopPolicy, AgentLoopPlanStepInput, AgentLoopRunStatus, AgentLoopStepStatus } from './AgentLoopService';
|
|
7
7
|
import { TokenTracker } from './TokenTracker';
|
|
8
|
-
import { getCircuitBreaker } from './CircuitBreaker';
|
|
8
|
+
import { getCircuitBreaker, subAgentCircuitKey } from './CircuitBreaker';
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
10
|
import { asObject, asArray, trimText, normalizeStepType, normalizePlanKey } from '../utils/ctx-utils';
|
|
11
11
|
|
|
@@ -816,7 +816,7 @@ export class AgentLoopController {
|
|
|
816
816
|
const target = step.target || '';
|
|
817
817
|
if (target) {
|
|
818
818
|
const circuitBreaker = getCircuitBreaker();
|
|
819
|
-
const state = circuitBreaker.getState(target);
|
|
819
|
+
const state = circuitBreaker.getState(subAgentCircuitKey(run.leaderUsername, target));
|
|
820
820
|
const attempt = Number(step.attempt || 0);
|
|
821
821
|
|
|
822
822
|
// Route away if: circuit is open, or 2+ failures and at least 1 retry already attempted
|
|
@@ -1044,7 +1044,13 @@ export class AgentLoopController {
|
|
|
1044
1044
|
runningSteps.map((step: any) => this.harness.executeStep(snapshot.run, step, options)),
|
|
1045
1045
|
);
|
|
1046
1046
|
|
|
1047
|
-
// Process results
|
|
1047
|
+
// Process results. Failed-but-retryable steps need a backoff before the
|
|
1048
|
+
// next iteration picks them up again. We compute the LARGEST backoff in
|
|
1049
|
+
// the batch and sleep once after the loop — never sum per-step sleeps.
|
|
1050
|
+
// Summing held the run-lock for up to maxConcurrency × 60s (= the whole
|
|
1051
|
+
// 5-minute lock when a full batch fails), starving other steps.
|
|
1052
|
+
let batchBackoffMs = 0;
|
|
1053
|
+
let approvalBreak = false;
|
|
1048
1054
|
for (let i = 0; i < results.length; i++) {
|
|
1049
1055
|
const runningStep = runningSteps[i];
|
|
1050
1056
|
const result = results[i];
|
|
@@ -1063,6 +1069,7 @@ export class AgentLoopController {
|
|
|
1063
1069
|
{ userId: options.userId, reason: 'Dynamic tool approval required by policy.' },
|
|
1064
1070
|
);
|
|
1065
1071
|
// Stop processing more results; remaining steps are left pending
|
|
1072
|
+
approvalBreak = true;
|
|
1066
1073
|
break;
|
|
1067
1074
|
}
|
|
1068
1075
|
|
|
@@ -1075,17 +1082,21 @@ export class AgentLoopController {
|
|
|
1075
1082
|
failedStep &&
|
|
1076
1083
|
Number(failedStep.attempt || 0) < Number(failedStep.maxAttempts || policy.maxStepAttempts)
|
|
1077
1084
|
) {
|
|
1078
|
-
// Exponential backoff
|
|
1085
|
+
// Exponential backoff — track the max; the step stays pending for retry.
|
|
1079
1086
|
const attempt = Number(failedStep.attempt || 1);
|
|
1080
1087
|
const baseDelay = 1000;
|
|
1081
1088
|
const maxDelay = 60000;
|
|
1082
1089
|
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
1083
|
-
|
|
1084
|
-
// Step remains pending for retry (failStep resets status back)
|
|
1090
|
+
batchBackoffMs = Math.max(batchBackoffMs, delay);
|
|
1085
1091
|
}
|
|
1086
1092
|
}
|
|
1087
1093
|
}
|
|
1088
1094
|
|
|
1095
|
+
// Single consolidated backoff for the whole batch (bounded at maxDelay).
|
|
1096
|
+
if (!approvalBreak && batchBackoffMs > 0) {
|
|
1097
|
+
await new Promise((resolve) => setTimeout(resolve, batchBackoffMs));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1089
1100
|
// Re-evaluate the plan — newly unblocked steps become eligible
|
|
1090
1101
|
snapshot = await this.getRunSnapshot(runId);
|
|
1091
1102
|
|
|
@@ -63,8 +63,8 @@ export class AgentLoopService {
|
|
|
63
63
|
this.plannerService = new AgentPlannerService();
|
|
64
64
|
this.validator = new AgentPlanValidator();
|
|
65
65
|
this.repository = new AgentLoopRepository(plugin);
|
|
66
|
-
this.harness = new AgentHarness(plugin, this.registryService);
|
|
67
66
|
const tokenTracker = new TokenTracker(plugin);
|
|
67
|
+
this.harness = new AgentHarness(plugin, this.registryService, tokenTracker);
|
|
68
68
|
this.controller = new AgentLoopController(
|
|
69
69
|
this.registryService,
|
|
70
70
|
this.plannerService,
|
|
@@ -2,6 +2,16 @@ import { AgentLoopPlanStepInput } from './AgentLoopService';
|
|
|
2
2
|
|
|
3
3
|
import { normalizeStepType, normalizePlanKey, asArray, asObject } from '../utils/ctx-utils';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic, template-based plan builder. This service does NOT call an LLM:
|
|
7
|
+
* it either passes through a caller-provided plan or emits a fixed
|
|
8
|
+
* prepare → (delegate|execute) → verify skeleton.
|
|
9
|
+
*
|
|
10
|
+
* The run's `plannerModel` field is metadata only (records which model an
|
|
11
|
+
* upstream caller used to author a provided plan); it does not drive any
|
|
12
|
+
* generation here. If LLM-authored planning is added later, it belongs in a
|
|
13
|
+
* separate path — keep this builder deterministic so it stays test-stable.
|
|
14
|
+
*/
|
|
5
15
|
export class AgentPlannerService {
|
|
6
16
|
buildPlan(
|
|
7
17
|
goal: string,
|